====== Trin 2: Inventory som genbrugelig data-komponent ======
[[trin1_opsaetning|← Forrige: Opsætning]] | [[start|Forside]] | [[trin3_slot_ui|Næste: Grafik til én slot →]]
I dette trin bygger du selve inventoryet: en komponent, der ved, hvilke items der ligger i hvilke slots. Der er endnu ingen grafik. Du tester med tekst i Output-panelet.
===== Hvorfor adskille data og UI? =====
Det er fristende at lade den samme klasse både gemme items og tegne dem. Det bliver dog svært at genbruge. Hvis inventoryet selv er en grafisk boks, kan en kiste, der ikke altid er synlig, ikke have ét uden at få en boks med.
Vi deler derfor opgaven:
* ''Inventory'' (dette trin) kender **kun data**: slots og items. Den har ingen grafik.
* ''InventoryUI'' (trin 4) kender **kun visning**: den får et ''Inventory'' at vide og tegner det.
Fordelene er, at du kan ændre udseendet uden at røre logikken, du kan teste logikken uden grafik, og samme ''Inventory'' kan bruges overalt: hos spilleren, i en kiste, hos en fjende eller i en butik.
===== Kort genopfriskning =====
Koden i dette trin bruger nogle ting, du kender fra tidligere:
* ''[Export]'' foran en property gør den redigerbar i Inspector.
* En **property** er en variabel med ''{ get; set; }'', som andre klasser kan læse og skrive.
* ''partial'' skal stå på klasser, der arver fra Godots typer.
* ''[GlobalClass]'' gør, at klassen kan vælges i editoren, fx under **Add Child Node**. Husk at trykke på **Build** først.
===== Først: InventorySlot =====
I stedet for en liste af items bruger vi en liste af **slots**. En slot er et lille objekt, der holder et item og et antal. Det virker som overkill, når hver slot højst rummer ét item, men det er netop det, der forbereder stacking: Den dag en slot skal rumme tre drikke, findes feltet ''Amount'' allerede.
Opret ''InventorySlot.cs'' i ''Scripts/Inventory'':
// En slot er ren data: den ved, hvilket item den holder, og hvor mange.
// Klassen arver ikke fra Godot-typer, så den er en helt almindelig C#-klasse.
public class InventorySlot
{
// Itemet i slotten. null betyder, at slotten er tom.
public Item HeldItem { get; set; }
// Antal items i slotten. Er altid 1 indtil videre (non-stackable),
// men feltet er klar til, at stacking kan tilføjes senere.
public int Amount { get; set; }
public bool IsEmpty()
{
return HeldItem == null;
}
public void Clear()
{
HeldItem = null;
Amount = 0;
}
}
**Forklaring:**
* Klassen har ikke ''using Godot;'' og arver ikke fra noget. Den er en helt almindelig C#-klasse og ikke en node eller ressource. Den behøver ikke Godot, fordi den kun gemmer data.
* ''null'' betyder "ingenting". Er ''HeldItem'' lig med ''null'', er slotten tom.
* ''IsEmpty()'' og ''Clear()'' gør koden andre steder lettere at læse: ''slot.IsEmpty()'' er tydeligere end ''slot.HeldItem == null''.
===== Så: Inventory =====
Opret ''Inventory.cs'' i ''Scripts/Inventory'' med dette indhold:
using Godot;
// Inventory er en genbrugelig komponent, der KUN håndterer data:
// hvilke items ligger i hvilke slots. Den ved intet om grafik.
// Den kan sættes som barn under en spiller, en kiste, en fjende osv.
[GlobalClass]
public partial class Inventory : Node
{
// Antal slots. Ændres i Inspector. Range begrænser tallet til 1-64.
[Export(PropertyHint.Range, "1,64,1")] public int SlotCount { get; set; } = 9;
// Items, inventoryet starter med. Praktisk til test.
[Export] public Godot.Collections.Array- StartItems { get; set; } = new Godot.Collections.Array
- ();
// Selve slotsene. "private" betyder, at kun denne klasse kan røre dem direkte.
// Alle andre skal bruge metoderne herunder, så reglerne samles ét sted.
private InventorySlot[] _slots;
public override void _Ready()
{
// Opret en tom slot for hvert felt i inventoryet.
_slots = new InventorySlot[SlotCount];
for (int i = 0; i < _slots.Length; i++)
{
_slots[i] = new InventorySlot();
}
// Læg start-items i inventoryet.
foreach (Item item in StartItems)
{
AddItem(item);
}
}
// Hvor mange slots har inventoryet?
public int GetSize()
{
return _slots.Length;
}
// Giver itemet i en slot, eller null hvis slotten er tom eller ikke findes.
public Item GetItem(int index)
{
if (!IsValidIndex(index))
{
return null;
}
return _slots[index].HeldItem;
}
// Lægger et item i den første tomme slot.
// Returnerer true, hvis det lykkedes, og false, hvis inventoryet er fuldt.
public bool AddItem(Item item)
{
if (item == null)
{
return false;
}
// Her kan stacking senere tilføjes: led først efter en slot med samme item,
// som ikke er fyldt op, og læg kun i en tom slot, hvis der ikke er nogen.
for (int i = 0; i < _slots.Length; i++)
{
if (_slots[i].IsEmpty())
{
_slots[i].HeldItem = item;
_slots[i].Amount = 1;
return true;
}
}
return false; // Ingen tomme slots
}
// Fjerner itemet i en slot og giver det tilbage.
// Returnerer null, hvis slotten var tom eller ikke findes.
public Item RemoveItem(int index)
{
if (!IsValidIndex(index) || _slots[index].IsEmpty())
{
return null;
}
Item removed = _slots[index].HeldItem;
_slots[index].Clear();
return removed;
}
// Skriver indholdet i Output-panelet. Bruges til at teste uden UI.
public void PrintContents()
{
GD.Print($"--- Inventory i '{GetParent().Name}' ({_slots.Length} slots) ---");
for (int i = 0; i < _slots.Length; i++)
{
string text = _slots[i].IsEmpty() ? "(tom)" : _slots[i].HeldItem.ItemName;
GD.Print($"Slot {i}: {text}");
}
}
// Tjekker, at et slot-nummer er inden for grænserne.
private bool IsValidIndex(int index)
{
return index >= 0 && index < _slots.Length;
}
}
**Forklaring af de nye begreber:**
* ''Inventory : Node'' gør komponenten til en node. Så kan den sættes som barn under en spiller eller en kiste i scenetræet, og Godot kalder automatisk dens ''_Ready()''.
* ''[Export(PropertyHint.Range, "1,64,1")]'' giver et talfelt i Inspector, der kun accepterer tal fra 1 til 64. Det er stedet, hvor du senere ændrer antallet af slots. Kisten kan have 9, og en rygsæk kan have 20, uden at koden ændres.
* ''_Ready()'' kaldes af Godot **én gang**, når noden og alle dens børn er klar. Her opretter vi slotsene. Du kalder den aldrig selv.
* Et **array** (''InventorySlot[]'') er en række af værdier af samme type med fast længde. ''_slots[0]'' er den første slot, ''_slots[1]'' den anden. Tælling starter altid ved 0.
* ''for'' gentager kode et bestemt antal gange. ''foreach'' gentager kode for hvert element i en samling.
* ''public'' og ''private'' bestemmer, hvem der må bruge et medlem. Arrayet ''_slots'' er ''private'', så andre klasser **skal** gå gennem metoderne ''GetItem'', ''AddItem'' og ''RemoveItem''. Så ligger reglerne ét sted. Når stacking senere skal tilføjes, ændrer du kun ''AddItem'', og alt andet virker uændret.
* ''bool'' er en værdi, der er enten ''true'' eller ''false''. ''AddItem'' returnerer ''true'', hvis der var plads. Den, der kalder metoden, kan dermed reagere, hvis inventoryet er fuldt.
* ''StartItems'' er en liste af items, som du udfylder i Inspector. Vi bruger den til test og til at give inventoryet noget indhold fra start.
* ''IsValidIndex'' beskytter mod fejl: Hvis nogen beder om slot 50 i et inventory med 9 slots, får vi ''null'' i stedet for at spillet går ned.
Tryk på **Build**, og kontrollér, at der ikke er fejl.
===== Sæt inventoryet på en spiller =====
Nu bruger du komponenten. Du laver en **Player-scene**, der har et ''Inventory'' som barn.
- Vælg //Scene → New Scene//. Under **Other Node** vælger du **Node** (den enkleste node) som rod.
- Omdøb roden til ''Player'' ved at dobbeltklikke på navnet.
- Højreklik på ''Player'', og vælg **Add Child Node**. Søg efter **Inventory**, og vælg den. Den står nu i listen, fordi klassen har ''[GlobalClass]'' og er bygget.
- Gem scenen som ''Scenes/Player.tscn'' (''Ctrl+S'').
Træet skal se sådan ud:
Player (Node)
└── Inventory (Inventory)
Vælg ''Inventory''-noden. Inspector viser nu **Slot Count** (9) og **Start Items**.
- Klik på **Start Items**, og sæt **Size** til 2.
- Træk to af dine item-filer (''.tres'') ind i element 0 og 1, fx et sværd og en drik.
Hvis du vil have mere end ét item i listen, så øg Size, og træk flere ind. Tomme felter ignoreres af ''AddItem''.
===== Midlertidigt script til test =====
Spilleren skal have et script, der giver adgang til inventoryet. Højreklik på ''Player''-noden, vælg **Attach Script**, vælg **C#**, og sæt stien til ''Scripts/Game/Player.cs''. Erstat derefter indholdet med denne midlertidige test-version. Linjen med ''PrintContents'' fjerner du igen i [[trin5_spiller|trin 5]]:
using Godot;
// Spilleren er en "ejer" af et inventory.
// Inventory-komponenten ligger som barn i Player-scenen.
public partial class Player : Node
{
public Inventory Inventory { get; private set; }
public override void _Ready()
{
// Find barnet med navnet "Inventory" i scenen.
Inventory = GetNode("Inventory");
// MIDLERTIDIGT: skriv indholdet i Output-panelet, så vi kan se, at det virker.
Inventory.PrintContents();
}
}
**Forklaring:**
* ''%%GetNode("Inventory")%%'' finder barnet med navnet ''Inventory'' og giver det tilbage som typen ''Inventory''. Teksten i parentesen er **stien** til noden, og den skal passe præcist til navnet i Scene-panelet.
* ''{ get; private set; }'' betyder, at alle må læse ''Inventory'', men kun ''Player'' selv må ændre det.
* Børn bliver klar //før// deres forældre. Derfor har ''Inventory'' kørt sin ''_Ready()'' og oprettet slotsene, når ''Player._Ready()'' kører. Rækkefølgen er vigtig, og vi bruger den igen i [[trin5_spiller|trin 5]].
===== Test =====
Tryk på **Build**, og kør derefter scenen med **Run Current Scene** (''F6''). Der kommer et tomt vindue. Kig i **Output**-panelet nederst. Du skal se noget i retning af dette (dine item-navne kan være anderledes):
--- Inventory i 'Player' (9 slots) ---
Slot 0: Sværd
Slot 1: Helbredelsesdrik
Slot 2: (tom)
Slot 3: (tom)
Slot 4: (tom)
Slot 5: (tom)
Slot 6: (tom)
Slot 7: (tom)
Slot 8: (tom)
Lav gerne små eksperimenter:
* Sæt **Slot Count** til 3 og tilføj tre start-items. Hvad sker der, hvis du tilføjer et fjerde? (''AddItem'' returnerer ''false'', og itemet bliver ikke lagt i inventoryet.)
* Sæt **Slot Count** tilbage til 9.
===== Tjekliste =====
* ''Player.tscn'' findes og indeholder et ''Inventory''.
* Output viser de rigtige items i de første slots.
* Der er ingen røde fejl i Output eller Build.
[[trin3_slot_ui|Næste: Grafik til én slot →]]