17 listopada 2024

Ładniejsze okna dialogowe

Jak wszyscy wiemy wygląd okien dialogowych w grze to bardzo ważna sprawa. Fajnie, aby były czytelne przejrzyste i nie wymagały analizy o co w nich w ogóle chodzi. W mojej gierce okienka były raczej... brzydkie. Musiałem coś z tym zrobić. Wymagało to zrozumienia jak pracować w Godot  z elementami, z których robi się gui.

Po przewinięciu kilku (słabych) tutoriali oraz obejrzeniu jednego dobrego (link na końcu wpisu)  wyszedłem od takiego menu aktualnych questów 


do nowego okienka   
 
Jak widać nadal jest true/false dla postępu w pod-zadaniach,
ale całość wygląda już dużo lepiej i nie zachodzi na pół ekranu

A tutaj nowe menu dialogowe:  

Mam jeszcze problem ze skalowaniem wielkości okna dialogowego
względem jego zawartości, ale przynajmniej już nie straszy jak wersja bez tekstur. 


Do wykonania teksturowanych grafik dla okien dialogowych wykorzystałem: Kenny ui-pack-adventure

Najlepiej wytłumaczone zasady tworzenia menu znalazłem u CocoCode na youtubie pod linkiem Create MAIN MENU for your Godot game

Inne tutoriale z tego kanału także są bardzo dobre i jeśli siedzisz przy godot oglądając znajdujące się tam wrzutki możesz zyskać sporo wiedzy.

15 listopada 2024

Quest Completed

Niemal udało mi się skończyć kod służący obsłudze questów. Na tę chwilę bohater nadal jest w stanie zająć się tylko jednym zadaniem, ale kod jest już przygotowany pod to, aby mógł ich otrzymać więcej. Poprawiłem mechanizm rozpoznawania, którą aktualnie kwestię powinien otrzymać od NPC w efekcie zmienił się format jsona, z którego czytam dane dla zadań przekazywanych graczowi. Dla urozmaicenia dorzuciłem muzyczkę grającą w pętli w tle podczas rozgrywki i zmieniającą się przy zakończeniu questa u NPC. Mechanizm umożliwia łatwe przełączanie utworów podczas wydarzeń z gry (przejścia między obszarami, wejścia w dany obszar czy np. walki)


Aktualny wygląd jsona opisującego sam quest z możliwościami dialogowymi poniżej. Nie do końca jestem przekonany w trzymaniu wskazania na startowy dialog dot. questa w tym pliku (pole EntryDialog) ale na razie spełnia on swoją funkcję. 

Jak łatwo można się domyślić RevisitDialog odpowiada za to co mówi NPC jeśli odwiedzimy go przed zakończeniem zadnia zaś  CompletedDialog za dialog gdy wszystkie podzadania są zakończone i otrzymamy nagrodę.

[
    {
        "Id": "CANDLES_RECOVER",
        "Title": "Odzyskaj lichtarze dla tajemniczej nieznajomej",
        "Description": "W swiatyni seta znajduja sie lichtarze,
nizeznajoma chce je odzyskac",
        "Giver": "Tajemnicza nieznajoma z karczmy",
        "GiverId": "MYSTERIOUS_STRANGER",
        "Status": "ACCEPTED",
        "EntryDialog": "res://assets/dialog/levels/taverupperlevel/
PrincessTalkOne.json",
        "RevisitDialog": "res://assets/dialog/levels/taverupperlevel/
PrincessTalkTwo.json",
        "CompletedDialog": "res://assets/dialog/levels/taverupperlevel/
PrincessTalkCompleted.json",
        "Tasks": [
            {
                "TaskCompleted": false,
                "TargetLocation": "SET_TEMPLE",
                "Type": "DISCOVER"
            },
            {
                "TaskCompleted": false,
                "Type": "FETCH",
                "TargetLocation": "SET_TEMPLE",
                "TargetNumber": "1",
                "TargetName": "CANDLE_01"
            }
        ],
        "Reward": {
            "Xp": 100,
            "Gold": 1000
        }
    }
]


Z innych ważnych zmian: 
- mocno skomplikował się plik zarządzania questami, 
- podobnie ilość sygnałów (events) jakie są do tego przypisane
- postać niezależna nie steruje już tak mocno samym oknem dialogowym (chociaż... trochę musi) oraz - - nie jest zależna od pliku z questami (a była co akurat było bez sensu)
- bez problemu mogę też dodawać dialogi do npc-tów, którzy nie przekazują żadnych questów.
- Poprawnie działa (raczej :) ) podsumowanie zadania i późniejsze interakcje z npc gdy nie ma już żadnego zadania dla gracza.


A poniżej kilka zrzutów

Dialog przy pierwszym spotkaniu, po którym możemy otrzymać zadanie.



Tutaj widać, że pod-zadania dla questa zostały wykonane - oba oznaczone jako TRUE.

Kolejno podsumowanie questa


I kolejne podejście - brak zadań u tego NPC.


Z ciekawych rzeczy które musiałem znaleźć to przypięcie się z poziomu kodu c# do sygnału przypisanego do węzła z godot. 
Poniżej jest to linia gdzie do węzła o typie AudioStreamPlayer2D podpinam się w sygnał Finished  (czyli zakończenia się grania dźwięku) i uruchamiam funkcję RemoveChildNode()


    private void QuestDone(String questDone)
    {
        AudioStreamPlayer2D questCompletedPlayer = new AudioStreamPlayer2D();
        questCompletedPlayer.Stream = ResourceLoader.Load<AudioStreamWav>("res://assets/audio/music/questcompleted.wav");
        questCompletedPlayer.SetVolumeDb(-30);
        questCompletedPlayer.Autoplay = true;
        questCompletedPlayer.Name = "questcompleted";
        AddChild(questCompletedPlayer);
        questCompletedPlayer.Play();
        backgroundPlayer.Stop();

        questCompletedPlayer.Finished += () => RemoveChildNode();
    }

13 listopada 2024

I naprawiłem

Pisanie poprzedniego postu pomogło mi w zrozumieniu co schrzaniłem w kolizjach. W sumie na plus. Okazało się, że miałem kilka błędów w sposobie definiowania warstw kolizyjnych na moich obiektach. No dobra, było poplątanie z pomieszaniem i działało tylko dlatego, iż praktycznie wszystkie elementy kolidowały ze czymkolwiek co było na ekranie.
Z tego też powodu musiałem utworzyć nową scenę testową ponieważ poprzednia do niczego się nie nadawała - były min. błędy w definicji kolizji TileMapy które uniemożliwiały poprawienie nieprawidłowo działających zderzeń z postacią. 

Aktualna scena testowa jest trochę biedna, ale działa jak trzeba. A i zmieniłem rysunki dla skrzynek oraz jak widać moje gui wyświetlające aktualny quest potrzebuje sporo miłości.

Obszar testowy gry w wersji 2.0.



p.s.

Bohater nadal nie potrafi podnosić przedmiotów z ziemi, a od tego problemu rozpocząłem zabawę.

11 listopada 2024

Popsułem grę ...

Trochę popatrzyłem w  kod i podczas implementacji podnoszenia przedmiotów (w kontekście realizacji questa) zacząłem analizować w jaki sposób mój bohater wykrywa kolizję z przedmiotami w grze. No i wyszło mi, że coś dziwnie. Stąd wyłączyłem kolizyjność  na poszczególnych warstwach oraz maskach (czymże są krok dalej) i nie potrafię jej teraz poprawnie włączyć!

Aktualnie postać przenika przez ściany, nie umie już otworzyć drzwi, pułapka inicjuje się na warstwie do której nie należy. Totalnie poplątałem warstwy kolizji i maski kolizji, a teraz mozolnie próbuję to naprawić. 

Trochę teorii

Model kolizji w godot opiera się na warstwach. Mamy warstwy, w której przedmiot z którym możemy wejść w interakcje istnieje w grze czyli tzw. CollisionLayer oraz warstwę, z którą przedmiot powinien kolidować tzw. Collision Mask. Do wyboru są możliwe 32 warstwy (!) stąd wynika, że ta funkcjonalność może być niebywale skomplikowana. 

Tak wygląda to w oknie edytora

Przykładowa kolizja ze ścianą

I teraz, aby nasz bohater potrafił kolidować np. ze ścianami, musi posiadać w definicji węzła CollisionShape2D dopasowane maski CollisonMask odpowiadające za CollisionLayer warstwy ścian. 

Dla ścian definiujemy iż są w grze na pierwszej warstwie kolizyjnej CollisionLayer :  

Warstwy kolizji można samodzielnie nazywać stąd też WALL

A nasz bohater (poniżej) musi mieć odpowiednio zdefiniowaną CollisionMask:

Aby kolidować ze ścianą w ramach CollisionMask wybieramy warstwę pierwszą. Zaznaczona
CollisionLayer o numerze 2 to warstwa na której znajduje się gracz

Tyle teorii

W ramach sprawdzenia napisałem projekcik gdzie postać po prostu łazi i koliduje. Okazało się, że czasem dziwacznie reaguje na zdefiniowane na ekranie obszary pewnie przez moją niewiedzę. Cóż walka o zrozumienie tematu trwa ponieważ dotyczy to całej rozgrywki, a nie chciałbym aby postać kolidował z każdym elementem na ekranie i każdy ekran zawsze był aktywny jako kolidujący z wszystkimi warstwami.

Tutaj kilka przykładów jak działają kolizje.

Gracz (CharactedBody2d) nie jest przypisany do żadnej warstwy kolizyjnej (CollisionLayer) ani nie sprawdza warstw z którymi może się zderzać (MaskLayer) - trzeba przyznać że dupne to nazewnictwo ale nie przeskoczę tego.

Efekt widać poniżej - 

Gracz (postać w ciemnym kolorze) wpada POD postać NPC 

NPC ma CollisionLayer w warstwie 5 (warstwę opisałem jako NPC)



Odznaczamy CollisionMask na 5 warstwie dla postaci gracza... 

I gracz traci możliwość wejścia za NPC


Teraz podobnie dla ścian - gracz nie koliduje ze ścianami (warstwa 1 w CollisionLayers) dla TileMap


Gracz przenika ścianę


 W edytorze to warstwa MaskLayer = 1 opisana jako WALL


Po odznaczeniu maski dla warstwy ścian (widać odznaczony checkbox na ekranie) gracz przestaje przenikać przez ściany


Ale za to pojawił mi się problem z wykrywaniem kolizji z użyciem tzw RayCast2D czyli promienia który powinien wykrywać kolizje z poszczególnymi warstwami. I o ile wykrywa kolizję z warstwą MUR  lub NPC ... to nie wykrywa mi kolizji z elementami, które są niby tak samo zdefiniowane a podpięte są pod węzły o typie  Area2D posiadające węzeł kolizyjny CollisionShape. I nie wiem dlaczego. Cóż posiedzę nad tym i może na coś wpadnę.


A bohater miał tylko podnosić przedmioty, a teraz to ... szkoda gadać


p.s.

Już wiem dlaczego RayCast2D nie wykrywał kolizji z Area2D ... ma na to specjalną opcję do odklikania nazwaną Areas. Bez niej nie wykrywa takich obszarów. Poniżej widać ją niemal na samym dole zrzutu ekranowego


I teraz przykład co było nie tak


RayCast2D ze źle zdefiniowanym wykrywaniem kolizji - mimo tego, że w konfiguracji powinien kolidować z tzw. GROUND_AREA (moja nazwa dla warstwy 4 CollisionLayer  na której umieściłem Arae2Dkolizji jednak nie ma.


I tutaj poprawna sytuacja - RayCast2D wykrył kolizję na warstwie 4 z elementem Area2D po odklikaniu opcji Areas w konfiguracji promienia. Widać to po tym jak świeci się na czerwono.





07 listopada 2024

Co u Sladuma cd - Quest

 Mozolnie pisze (zbyt) skomplikowany mechanizm questów. Kod wygląda na tworzony do poważnej gry, a nie jako element gry prostej.  Ale cóż zrobić. Tak wyszło. 

Aktualny poziom zaawansowania pozwala na wyświetlenie zadania głównego, jego pod-zadań do wykonania oraz obsługa taska o roboczym typie DISCOVERY, którego celem jest udanie się do określonego na mapie obszaru.

Kolejny podtask, który posiada prawie całą obsługę jest FETCH - czyli zbieractwo przedmiotów. W tym celu będę musiał stworzyć podstawową implementację plecaka postaci, a ta nie istnieje nawet w powijakach.

A tak powyższe wygląda po zaimplementowaniu :

Po odebraniu zadania od NPC wyświetla się ono jako aktywny w lewym górnym rogu wraz z pod-zadaniami oraz stopniem ich wykonania.
Stan wykonania zadań roboczo opisany jest etykietką z napisem: true/false


Po dotarciu do interesującego nas obszaru - tutaj nazwanego Świątynią Seta, status zadania o typie DISCOVERY zmienia się na true , czyli zostało wykonane!


06 listopada 2024

Jak zatrzymać gracza w godot, ale nie grę?

Tym razem krótki poradnik :)
Zdarza się iż gracz powinien czekać na działania NPC lub po prostu stać w miejscu do momentu np.: zakończenia rozmowy z NPC, odpalenia menu czy zwyczajnej pauzy. 
Stąd pojawia się powód do odebrania możliwość ruchu i zamiast skazywać się na mozolne blokowanie  klawiatury i myszy można to zrobić poprzez wywołanie trzech prostych funkcji wbudowanych w godot dostępnych dla wszystkich obiektów rozszerzających klasę Node

Poniżej przykład jak zablokować gracza

    public void Disable()
    {
        SetPhysicsProcess(false);
        SetProcess(false);
        SetProcessInput(false);
    }

Oczywiście aby go odblokować robimy to samo tylko na odwrót :)

    public void Enable()
    {
        SetPhysicsProcess(true);
        SetProcess(true);
        SetProcessInput(true);
    }

Po wywołaniu metod gra działa samodzielnie - nie wyłącza się muzyka, NPC mogą spokojnie dalej łazić po planszy (o ile to zaprogramowaliśmy), ale za to gracz nie ruszy się z miejsca.

01 listopada 2024

C# i JSON grafy skierowane i konwersacje w grze

Podczas pisania kodu do dowolnej gdy trzeba zrobić system odczytu plików związanych z jej logiką. Mogą to być zapisane stany gry, statystyki potworów, opisy postaci, pliki lokalizacji, struktura mapy etc... Możliwości jest multum.  Stąd też potrzebny jest odczyt oraz odpowiednie formatowanie przechowywanych danych. Ze względu na składnię i lekkość oczywiście najlepiej użyć plików tekstowych w formacie json. 

Taki też problem pojawił się u mnie gdy chciałem utworzyć pierwszą konwersację Sladuma ze zleceniodawcą. W skrócie co trzeba było zrobić:

  • przypomnieć sobie czym są grafy skierowane, z grafów łatwo wyjść do konwersacji...
  • stąd stworzyć plik w formacie json zawierający opis pojedynczej konwersacji powiązany z daną postacią gdzie wstępna struktura obejmuje możliwość dodania tekstu do postaci i odpowiedzi jakie ma przed sobą gracz 
  • podstawowy system eventów, które generują kliknięcia w opcje odpowiedzi na ekranie np. przyjęcie questa czy zamknięcie okna konwersacji
  • treść konwersacji kod powinien automagicznie :) przekształcać w klasy 
  • te powinny zostać przeniesione na ekran w odpowiednim momencie (gracz podchodzi do postaci z którą można rozmawiać i ma możliwość zainicjowania rozmowy)
  • gracz klika po opcjach podczas trwania konwersacji
  • przechodzi konwersacje i przyjmuje (lub nie) zadanie od NPC, a gra działa dalej
A teraz trochę kodu:

Plik jsona z przykładową konwersacją (na razie jest to wstępny format pliku, pewnie dojdzie tu trochę opcji). Plik leży sobie gdzieś w assetach gry. To wstępny format i zakres pól, ale w pełni umożliwiający przeprowadzenie konwersacji

[
    {
        "Id": "A1",
        "DialogDescription": "Tajemnicza nieznajoma",
        "Dialog": "Mikołaju! W tej karczmie wyglądasz mi na jedynego który
jest w stanie podjąć się niezwykle ważnego dla mnie zadania. Nie chcę ,
żebyś narażał się za darmo, jeżeli zgodzisz mi się pomóc otrzymasz
sutą nagrodę. Czy chcesz wysłuchać o co chodzi?",
        "Event": "PROCEED",
        "Targets": [
            {
                "SourceId": "A1",
                "DestinationId": "A2",
                "Dialog": "Oczywiście masz rację",
                "Event": "PROCEED"
            },
            {
                "Dialog": "Nie chce mi się z tobą gadać",
                "Event": "END_DIALOG"
            }
        ]
    }, ...... i tam dalej są kolejne opcje konwersacji

Tutaj przykład do jakiej klasy przekształcany jest powyższy json:

namespace Dialog
{

    using Godot;
    using System;

    public partial class Conversation : Node
    {
        public string Id { get; set; }
        public string DialogDescription { get; set; }
        public string Dialog { get; set; }
        public string Event { get; set; }
        public ConversationChoice[] Targets { get; set; }
    }
    public enum DialogEvent
    {
        PROCEED, END_DIALOG, QUEST_ADD, QUEST_ACCEPTED
    }
}


i pochodna ConversationChoice

namespace Dialog
{

    using Godot;
    using System;

    public partial class ConversationChoice : Node
    {

        public string SourceId { get; set; }
        public string DestinationId { get; set; }
        public string Dialog { get; set; }
        public string Event { get; set; }
        public string QuestId { get; set; }

    }
}


Tutaj elegancki (zauważ generyczny) konwerter jsonów

public class JsonReader<T>
    { 
        public T ReadJsonFile(string fileName)
        {
            using FileAccess acces = FileAccess.Open(fileName,
                                 FileAccess.ModeFlags.Read);
            var text = acces.GetAsText();
            T readedJson = JsonSerializer.Deserialize<T>(text);
            return readedJson;
        }
    }

Zainicjowanie konwertera dzejsońów jako dialogReader  i wywołanie w kodzie (tu akurat inicjowanie dialogu w ramach klasy opisującej NPC). Trochę drewniane jest zaszywanie ścieżek do plików w głównym pliku npc-ta, ale to się poprawi :)


    private String DialogFilePathOne =
        "res://assets/dialog/levels/taverupperlevel/PrincessTalkOne.json";
    private JsonReader<List<Dialog.Conversation>> dialogReader = new();
   
    public void InitDialog()
    {
        convesations = dialogReader.ReadJsonFile(DialogFilePathOne);
    
.... reszta kodu dalej....


i efekt wizualny na ekranie po dodaniu tekstu do okienek dialogowych (nie umieszczonych :D ) 




A tu pod okienkiem gry,  w konsoli VS studio widać, że postać przyjęła zadanie zakceptowana misja QUESTS ID: CANDLES_RECOVER  Nad obsługą questów dopiero pracuję...




p.s.
Jakby ktoś chciał jak wygląda samo okno dialogowe i co tam się dzieje ... to niech da znać.

29 października 2024

Co u Sladuma ...

A dzieje się, dziękuję.


W celu ułatwienia sobie sprawdzenia czy to co dodaję działa stworzyłem scenę testową, na której umieszczam tworzone elementy. I na tę chwilę postać umie:
  • otwierać skrzynki
  • otwierać drzwi 
  • dla testu dodałem pułapkę podłogową z dwoma stanami (no w sumie mam tylko dwa rysunki animacji) i potrafi zadać obrażenia bohaterowi (ten nie umiera bo tego jeszcze nie obsługuję)
  • i chyba najfajniejsze - od nowa zrobiłem okno dialogowe bo poprzednie nie działało dobrze
  • dialog wczytywany jest z pliku json
  • tekst wyświetla się w okienku 
Na razie dialog w pełni nie działa, ale jest OK. Dodatkowo dzięki sztuczce podpatrzonej w ramach 
https://www.youtube.com/watch?v=WaotOuDNio8

okienko pojawia się ALE animacje w grze kręcą się dalej.

 

Zrzucik z opisami 






27 października 2024

Czy ten Node2D to na pewno Player.cs?

Sprawdzałem jak szybko ładnie sformatować jakikolwiek kawałek kodu w bloggerze i trzeba przyznać, że bez zewnętrznego wsparcia blogger takiej funkcji nie daje. Stąd, po krótkich poszukiwaniach w celu znalezienia formattera trafił na jakąś stronkę takowy udostępniającą. W sumie to wstyd, że taki portal nie ma tej opcji wbudowanej... Może trzeba poszukać jakiegoś innego miejsca na swoje wpisy? 
Kod poniżej dalej wygląda jak kupa, ale przynajmniej trzyma tabulacje


Whatever, bo nie o tym miało być.

Pisząc w Godot, ale używając C# często i gęsto natknie się człowiek na problem jaki to typ obiektu wpadł Ci do metody i czy możesz coś z nim zrobić. 

Godot pod tym względem jest... powiedzmy... automagiczny jak każdy słabo typowalny język. Radzi sobie z automatycznym typowaniem węzłów i powiązanych z nimi skryptów i nie robi przeszkód. Czasem zrzuci błąd, ale nie martw się - dowiesz się o nim w runtime gdy twój mozolnie cyzelowany kawałek kodu wyloguje do konsoli tysiące błędów w 10 sec.

W C# zaś jesteśmy skazani na ostrą typowalność zmiennych i takie cuda jak int raz będący Stringiem a jednocześnie Node2D nie przejdą. Stąd trzeba sobie radzić i rzutować. 


Poniżej przykład klasy, która w oparciu o generyki zrzutuje obiekt godota na coś sensownego (lub nie ;p )


using Godot;
using System;
public static class VariantTester<T>
{
    public static T GetType(Variant unknown)
    {
  
        	switch (unknown.VariantType)
        {
            case Variant.Type.Nil:
                break;
            case Variant.Type.Int:
                break;
            case Variant.Type.Object:
                return CheckIsT(unknown);
        }
        return default;
    }

    private static T CheckIsT(Variant unknown)
    {
        try
        {
            if (unknown.AsGodotObject() is T matching)
            {
                return matching;
            }
            else
            {
                return (T)Convert.ChangeType(unknown.AsGodotObject(), typeof(T));
            }
        }
        catch (InvalidCastException)
        {
            return default;
        }
    }

}

  
Wywołanie tego kawałka kodu jest proste jak drut - tak jak i późniejsze użycie

// jakas tam klasa gdzie nam wpada Node2D do funkcji
public void OnBodyIn(Node2D node)
    {
        if (node.IsInGroup("Player")) // ulatwijmy sobie
        {

// proba rzutowania // sprawdzenie null-safe w c# i wywolanie metody na znalezionej klasie :)
        VariantTester<Player>.GetType(node)?.AddItemToInventory(itemResource); 
        }

    }
Na razie na coś wiele mądrzejszego nie wpadłem (w sumie C# używam jakieś 3 tygodnie także miejcie litość) , a jak macie lepszy pomysł to zapraszam do komentarzy 
Działa też w przypadku gdy szukami interfejsu .

26 października 2024

Co u Sladuma?

Postaram się w krótkich postach opisywać co zmieniło się w grze pomiędzy nimi. Ten będzie pierwszy i miejmy nadzieję że nie ostatni.

Powstał plan co po kolei jest mi potrzebne do tego, aby gra powoli powstawała. Spytacie (spytacie ?) Dlaczego? Powód jest prosty - żeby nie próbować robić wszystkiego jednocześnie bo inaczej do niczego się nie dojdzie. Najpierw najważniejsze rzeczy na początek, później te mniej ważne. 

Ale oczywiście ... różnie z tym bywa. Aktualnie pracuję nad tym, aby pierwszy poziom po którym łazi Sladum powoli stał się interaktywny i zawierał jak najwięcej funkcjonalności które będę mógł reużyć na  kolejnych planszach.

Mam więc: 
Średnio działającą maszynę stanów (czasem się gubi i ładuje nie tę animację co trzeba), 
wstępną tilemapę dla pierwszego piętra tawerny oraz dla sali czyli parteru (to w sumie są dwie plansze a nie jedna jak pisałem powyżej)
 jakieś oświetlenie 
`fadeScreen` który powoduje że przejście między poziomami nie jest momentalne, a opiera się na krótkiej animacji ściemniającej ekran, zmianie poziomu i ponownie odciemniającej się.
Jakieś podstawowe dźwięki ponieważ lubię coś słyszeć w tle. Nawet na tak wczesnym etapie gra bez jakiegokolwiek udźwiękowienia to katorga dla twórcy :) Niech będzie minimum - kroki bohatera i jakieś tło dla dźwięków tawerny...



I taki zrzut ekranu. Piętro tawerny. Efekt zaciemniania ekranu nie dało się nagrać

i sala główna tawerny