Pierwsze kroki z SadConsole

Czas zacząć pracę na grą Mars-Buggy. Najpierw zobacz mój post Mars-Buggy - Daj Się Poznać 2017. Moje posty będą miały nieco tutorialową formę, aby ułatwić mi samemu pracę z SadConsole w przyszłości.

Instalowanie SadConsole

Na ten moment będę korzystał z dwóch paczek NuGetowych: SadConsole.Core.MonoGameGL i SadConsole.GameHelpers.MonoGameGL. Pierwsza zawiera cały framework do renderowania i emulacji konsoli w oknie OpenGL, a druga zawiera dodatkową logikę, która usprawnia korzystanie z frameworka.

Uruchamianie aplikacji

Utworzyliśmy nowy projekt aplikacji konsolowej w F# i dodaliśmy dwie paczki NuGetowe. W folderze mamy Program.fs oraz 6 plików - 3 pary - czcionek do SadConsole.

Na Linuxie trzeba ręcznie ustawić CopyToOutputDirectory dla plików czcionek

Czas dodać kilka linijek do naszej funkcji main:

open SadConsole.Consoles
open Microsoft.Xna.Framework

[<EntryPoint>]
let main argv = 
    SadConsole.Engine.Initialize("IBM.font", 80, 25);

    SadConsole.Engine.EngineStart.Add (fun _ ->
            let console = SadConsole.Engine.ActiveConsole :?> Console
            console.Print(1, 1, "Hello World!"))

    SadConsole.Engine.Run()
    0 // exit code

Najpierw inicjalizujemy nasz silnik konsoli domyślną czcionką (IBM.font) oraz rozmiarem okna 80x25. Następnie subskrubujemy event EngineStart, podczas którego wypiszemy na konsoli “Hello World!” zaczynając w drugim wierszu i drugiej kolumnie (licząc od 0).

Naszym oczom ukaże się:

Pętla gry

SadConsole jest frameworkiem, co oznacza, że my tylko dostarczamy kod, który aplikacja będzie wykonywać. Dlatego nie my decydujemy o pętli gry, a robi to za nas MonoGame, z którego SadConsole korzysta.

Ponieważ SadConsole było tworzone dla C#, to oczywiście jest zorientowane obiektowo. Pracując dłuższą chwilę w środowisku funkcyjnym musiałem trochę przestawić swój tok myślenia i wrócić do obiektowego paradygmatu. A w zasadzie to je połączyć.

SadConsole pozwala na utworzenie kilku niezależnych konsoli, z których każda będzie na ekranie wyświetlana w innym miejscu. Mi jest to na razie nie potrzebne, więc będę miał tylko jedną konsolę MainConsole.

type MainConsole(width, height) =
    inherit Console(width, height)

    override this.Update() = ()

    override this.Render() = ()

Dziedziczymy po typie SadConsole.Consoles.Console i nadpisujemy jego metody Update i Render. Są to elementy naszej pętli gry. Jedyne czego brakuje to pobieranie inputu od użytkownika, ale o tym powiemy sobie w przyszłym tygodniu.

Obiekty renderowania

Moim celem na początek jest wypisać na konsolę duży napis “Mars-Buggy”. Chciałbym zawrzeć to w instancji SadConsole.Game.GameObject, ponieważ pozwala to mi na łatwe renderowanie obiektu na ekranie. Kiedy używamy metody Print konsoli, to żeby przesunąć napis, trzeba najpierw wymazać stary, bo inaczej będą dwa. Używając metody Render na obiekcie GameObject zostanie to zrobione za nas.

Na ten moment tworzenie tych obiektów, a dokładnie animacji jest nieco pracochłonne, więc będę tworzył sobie jakieś metody pomocnicze, ale tego kodu używam w tej chwili:

let Nullable<'a when 'a:(new:unit->'a) and 'a:struct and 'a:>System.ValueType> (x:'a) = System.Nullable x

let editorFill (editor:SurfaceEditor) (fg:Color) (bg:Color) (glyph:int) =
    editor.Fill(Nullable fg, Nullable bg, Nullable glyph) |> ignore

let welcomeScreen = 
    let mainText = GameObject(Engine.DefaultFont)

    let textSurface = AnimatedTextSurface("default", 43, 14)
    let frame = textSurface.CreateFrame()
    let editor = SurfaceEditor(TextSurface(1, 1, Engine.DefaultFont))

    editor.TextSurface <- frame
    editorFill editor Color.OrangeRed Color.Transparent 0

    editor.Print(0, 0, "MM     MM     A     RRRRR      SSS ")
    editor.Print(0, 1, "M M   M M    A A    R    R    SS  S")
    editor.Print(0, 2, "M  M M  M   A   A   RRRRR     SS   ")
    editor.Print(0, 3, "M   M   M   AAAAA   R R        SSS ")
    editor.Print(0, 4, "M       M  A     A  R  R     S   SS")
    editor.Print(0, 5, "M       M A       A R   RR    SSSS ")
    editor.Print(0, 6, "                                   ")
    editor.Print(0, 7,  "BBBBBB   U     U   GGGGG    GGGGG   Y     Y")
    editor.Print(0, 8,  "B     B  U     U  G     G  G     G   Y   Y")
    editor.Print(0, 9,  "BBBBBB   U     U  G        G          Y Y")
    editor.Print(0, 10, "B     B  U     U  G   GGG  G   GGG     Y")
    editor.Print(0, 11, "B     B  U     U  G     G  G     G    Y")
    editor.Print(0, 12, "BBBBBB    UUUUU    GGGGG    GGGGG   YY")

    mainText.Animation <- textSurface
    mainText.Position <- Point(20, 4)
    
    mainText

Po kolei:

  1. tworzę nowy GameObject,
  2. tworzę nową animację AnimatedTextSurface,
  3. dodaję do niej pierwszą klatkę CreateFrame(),
  4. tworzę edytor SurfaceEditor, który ułatwia edytowanie klatek,
  5. ustawiam editor.TextSurface na bierzącą klatkę,
  6. ustawiam kolor tekstu (OrangeRed) i tła (Transparent), korzystając z funkcji pomocniczej editorFill,
  7. wypisuję to co chcę do edytora, który umieszcza to na klatce
  8. przypisuję animację do mojego obiektu,
  9. ustalam jego pozycję,
  10. przypisuję go do stałej welcomeScreen.

Pozycja obiektu jest relatywna do ekranu, a nie do konsoli. Jeśli nasza konsola ma pozycję inną niż (0,0) lub pole renderowania to musimy to przesunięcie uwzględnić.

Teraz w mojej konsoli ustawiam

override this.Render() = welcomeScreen.Render()

Pamiętaj, że w F# ważna jest kolejność deklaracji, więc MainConsole musi być zdefiniowany poniżej welcomeScreen.

Na koniec dokładamy to co zrobiliśmy do funkcji main

let main argv = 
    SadConsole.Engine.Initialize("IBM.font", 80, 25);

    SadConsole.Engine.EngineStart.Add (fun _ ->
            SadConsole.Engine.ConsoleRenderStack.Clear()
            SadConsole.Engine.ConsoleRenderStack.Add(MainConsole(80, 25)))

    SadConsole.Engine.Run()
    0 // return an integer exit code

Wynik: