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}