Sztuka wrappowania

13 Marca 2017

Bardzo często pracując z cudzą biblioteką zetkniemy się z tym, że dane rozwiązanie jest szersze, bądź inaczej zrobione, niż to czego potrzebujemy. Dlatego warto napisać wrapper, czyli jakiś interfejs dostępu do funkcjonalności biblioteki, ale na naszych warunkach.

W moim kontekście chcę zrobić wrapper wokół mechanizmu tworzenia animacji. W poprzednim poście napisałem długi (jeden ekran) kawałek kodu, który tworzy mój napis. Zacząłem więc myśleć jak to uprościć. W efekcie końcowym będę tworzył obiekt za pomocą jednej funkcji.

Funkcja na każdą okazję

Utworzyłem nowy moduł i zacząłem tworzyć funkcje. Najpierw funkcja create, która zwraca nowy obiekt

let create () = GameObject(Engine.DefaultFont)

Mały zysk w ilości kodu, ale nie muszę się martwić jeśli nagle stwierdzę, że chcę każdemu obiektowi ustawiać własność X przy tworzeniu.

Podobnie z tworzeniem animacji

let animationWithName name width height = AnimatedTextSurface(name, width, height)

let animation = animationWithName "default"

Raczej będę używał tylko animation, ale jeśli zajdzie potrzeba dodania nazwy, to mam taką możliwość. Można tu też zauważyć, że korzystam z częściowej aplikacji funkcji animationWithName.

Dalej mam funkcję tworzącą nową klatkę

let addFrame (animation:AnimatedTextSurface) = animation.CreateFrame()

Oraz funkcją tworzącą edytor z tą klatką

let editor frame = SurfaceEditor(frame)

Więc jak widać, niespecjalnie tu uprościłem korzystanie z dostarczonego przez bibliotekę interfejsu, ale cała magia pojawi się zaraz.

Tworzenie animacji

Obiekty będą animowane. Jak będę te animacje przechowywać? W plikach tekstowych, które zostaną wkompilowane w aplikację, opcją Embedded resource. Taki plik będzie zawierać tekstową animację, gdzie każda klatka będzie oddzielona specjalnym znakiem:

let animationFrameSeparator = "𐆀"

Moja funkcja dostanie na wejściu tablicę linii takiego pliku, a następnie korzystając z wcześniej zadeklarowanych funkcji utworzy mi animację o wielkości automatycznie wykrytej na podstawie danych.

Pozostało mi przedefiniować funkcję editorFill z wcześniej:

type Surface = {
                Foreground: Color
                Background: Color
                Glyph: int
            }

let defaultSurface = { Foreground = Color.White; Background = Color.Transparent; Glyph = 0}

let editorFill (editor:SurfaceEditor) surface =
    editor.Fill(System.Nullable surface.Foreground,
                System.Nullable surface.Background,
                System.Nullable surface.Glyph) |> ignore

Oto funkcja a poniżej wyjaśnienia:

let loadAnimation (text:string array) surface =
    let width = text |> Array.map (fun line -> line.Length) |> Array.max
    let height =
        let rec heightCount pos h current =
            if pos >= text.Length then (max h current)
            else 
                if text.[pos] = animationFrameSeparator then
                        heightCount (pos + 1) (max h current) 1
                else
                        heightCount (pos + 1) h (current + 1)
        heightCount 0 0 1
    let anim = animation width height
    let rec processText pos =
        let edit = addFrame anim |> editor
        do editorFill edit surface
        let rec fillFrame pos line =
            if pos >= text.Length then None
            else
                if text.[pos] = animationFrameSeparator then Some (pos + 1)
                else
                    do edit.Print(0, line, text.[pos])
                    fillFrame (pos + 1) (line + 1)
        match fillFrame pos 0 with
        | None -> ()
        | Some npos -> processText npos
    do processText 0
    anim

Na początku obliczam szerokość i wysokość mojej animacji na podstawie maksymalnej długości wierszy i maksymalnej ilości wierszy w pojedynczej klatce. Potem tworzę nową animację o tych rozmiarach. Następnie dla każdej klatki tworzę edytor, wypełniam go powierzchnią dostarczoną w parametrze funkcji i przechodzę się po kolejnych wierszach tekstu, wypisując ich zawartość do edytora. Jeśli napotkam znak separacji animacji, to rekurencyjnie tworzę następną klatkę, zaczynając od kolejnego wiersza.

Uff, sporo roboty, ale się opłaca.

Tworzenie obiektów

Teraz jeszcze dwie funkcje, które zapewnią mi obiekty, które od razu będą miały animację:

let createWithAnimation x y text surface =
    let entity = create ()
    entity.Animation <- loadAnimation text surface
    entity.Position <- Point(x, y)
    entity

let createWithAnimationFromFile x y animName surface =
    createWithAnimation x y (Data.loadAnim animName) surface

W createWithAnimation tworzę obiekt, tworzę animację, ustawiam mu pozycję i voila. W drugiej funkcji to samo tylko zamiast tekstu animacji (typu string array), bierzemy nazwę animacji, a dokładniej pliku. Widzimy tu jeszcze moduł Data, który wygląda następująco:

module Data

open System.Reflection
open System.IO

let assembly = Assembly.GetExecutingAssembly()

let openEmbeded name = 
    assembly.GetManifestResourceStream(name)

let loadAnim name =
    use stream = openEmbeded (name + ".anim")
    use reader = new StreamReader(stream)
    reader.ReadToEnd().Split('\n')

Podsumowanie

Celem tego tekstu jest pokazanie jak można owinąć (wrap) cudze API, tak aby można było z niego w prosty sposób korzystać. W tej chwili mój welcomeScreen to de facto jedna linijka kodu (no prawie). Ponadto każdy kolejny obiekt, to również jedna linijka kodu. Więc zyskujemy na czytelności. Ważne jest aby nazwa funkcji mówiła co robi, dlatego takie długie nazwy jak createWithAnimationFromFile są jak najbardziej pożądane.

let welcomeScreen = 
    createWithAnimationFromFile 20 4 "welcomeScreen"
    <| {defaultSurface with Foreground = Color.OrangeRed}