[Długie] Lista obiektów różnych typów - zapis/odczyt do pliku.

0

Witam.

W charakterze "wprawki" piszę sobie grę rougelike i napotkałem na pewien problem:

Dla przykładu - mam między innymi klasy:

public abstract class GameObject
{
	public Point position;
	public abstract void load(string objectData); // ma stworzyć obiekt na podstawie linijki tekstu (odczytanej z pliku) <=====
	public abstract string save(); // ma zapisać zawartość obiektu do linijki tekstu (zapisanej później do pliku)
	public abstract draw();
	//......
} 

Od razu pytanie numer 1: Czy da się wymusić, aby wszystkie klasy potomne posiadały konstruktor przyjmujący 1 argument typu string?

public class GameObjectDoor:GameObject
{
	public bool isOpen;
	public override void load (string objectData) {/*....*/};
	public override string save() {/*....*/}; 
	//......
} 
public class Creature:GameObject
{
	public int hitPoints;
	public int maxHitPoints;
	public override void load (string objectData) {/*....*/};
	public override string save() {/*....*/}; 
	//......
} 

I tak dalej, i tak dalej. Z klasy Creature z kolei dziedziczyć będą później np. klasy Wolf, Rat i Dragon. Ogólnie - tych klas będzie dużo. Chcę, żeby to było rozszerzalne.

Najbardziej interesują nas w tym momencie metody string save() i void load(string) - chodzi o to, aby zapisać wszystko do pliku. Przyjąłem sobie, że w tym pliku każda linijka odpowiada jednemu obiektowi, i zaczynać się będzie od nazwy typu (oczywiście, można to zmienić).

Wszystkie obiekty przechowywane są w liście:

List<gameObject> objectsOnMap;

Zapis gry do pliku jest w tej sytuacji zaskakująco trywialny:

public void save(string fileName)
{
            StreamWriter sW = new StreamWriter(fileName);
            foreach (GameObject obj in objectsOnMap)
            {
                sW.WriteLine(obj.save());
            }
            sW.Close();
}

Problem pojawia się przy odczycie - na chwilę obecną wygląda to tak:

public void load(string fileName)
{
	StreamReader sR = new StreamReader (fileName);
	string line;
	while (sR.Peek() >= 0)
{
		line = sR.ReadLine();
		switch(line.Split('\t')[0])
		{
			case "GameObjectDoor":
				objectsOnMap.Add(new GameObjectDoor(line))
				break;
				
			case "Creature":
				objectsOnMap.Add(new GameObjectDoor(line))
				break;
		}
	}
	sW.Close();
}

STRASZNIE nie podoba mi się ten switch w tej funkcji - powoduje to, że za każdym razem, gdy będę dopisywał nową klasę obiektu, będę musiał (oprócz napisania samej klasy, oraz metod do zapisu/odczytu z/do stringa dla niej) , zmodyfikować także zupełnie inną klasę, gdzie znajduje się definicja funkcji czytającej listę obiektów z pliku. Mam nieodparte wrażenie, że dałoby się całość zorganizować tak, aby wczytywanie było równie "eleganckie", jak zapis.

Pytanie numer 2: Czy da się to jakoś rozwiązać, tak, aby jedyne zmiany w kodzie były tylko w klasie potomnej?
Pytanie numer 3: Czy da się iterować po wszystkich typach (!) dziedziczących z GameObject? (pewnie się nie da, ale nie zaszkodzi spytać)

Pytanie numer 4:
Mamy klasy:

class  GameObject {…}
class  Door:GameObject {…}
class Monster:GameObject {…}
class Dog:Monster {….}
class Rat:Monster

I funkcję:

void foo(GameObject  obj)

Pytanie numer 4: Jak napisać warunek IF, który będzie spełniony, jeżeli zmienna obj jest typu Monster (czyli także Rat lub Dog)?

1

Ad. 1. Skorzystaj z metody opisanej tutaj: http://www.parashift.com/c++-faq-lite/serialize-inherit-no-ptrs.html (nie ma większego znaczenia, że to C++).
Ad. 2. Będziesz musiał dodawać prototypy albo fabryki/funkcje tworzące dany typ do wspólnej mapy, ale da się to zrobić ze statycznego konstruktora.
Ad. 3. Przyda się Assembly.GetTypes() i Type.IsAssignableFrom()
Ad. 4. if(obj is Monster)

Ewentualnie, jeżeli nie przeszkadza ci inna forma zapisu tych obiektów w pliku to możesz użyć serializacji z .NET (BinaryFormatter albo XmlFormatter).

2

To ja ponarzekam sporo pewnie ;]

Od razu pytanie numer 1: Czy da się wymusić, aby wszystkie klasy potomne posiadały konstruktor przyjmujący 1 argument typu string?

Nie - po co takie ograniczenie? I tam nie wywołasz polimorficznie konstruktora.

I tak dalej, i tak dalej. Z klasy Creature z kolei dziedziczyć będą później np. klasy Wolf, Rat i Dragon. Ogólnie - tych klas będzie dużo. Chcę, żeby to było rozszerzalne.

Rozszerzalne, to by była jedna, klasa Creature i żadnych klas dziedziczących. Trick polega na tym że dla każdego potwora tworzy się kilka skryptów opisujących zachowanie - ale to dużo roboty, dlatego nie namawiam Cię do implementacji tego ;].

Najbardziej interesują nas w tym momencie metody string save() i void load(string) - chodzi o to, aby zapisać wszystko do pliku. Przyjąłem sobie, że w tym pliku każda linijka odpowiada jednemu obiektowi, i zaczynać się będzie od nazwy typu (oczywiście, można to zmienić).

Nie brzmi to zbyt dobrze - implementując to naiwnie, w każdej klasie będziesz musiał sobie od zera parsować całe dane.
Żeby to się dało dość wygodnie parsować, najlepiej zainwestować w format przechowujący pary klucz:wartość. Możliwość pierwsza - XML. Ale jeśli chcesz coś prostszego, możesz użyć czegoś takiego na przykład:

monster-type:Rat; hp:50; max-hp:50;
monster-type:Dragon; hp:450; max-hp:500;
monster-type:Pig; hp:5; max-hp:5;

Etc - teraz masz jedną linijkę na potwora, a jednocześnie można to łatwo parsować - metoda load(string) zamienia się w:

 public override void load (Dictionary<string, string> objectData) {/*....*/};

Tutaj oczywiście zrobisz co chcesz, ja tylko dobrze radzę ;]

Da się jeszcze prościej - możesz to zrobić za pomocą serializacji obiektów, wtedy się nie będziesz musiał w ogóle męczyć ;]

STRASZNIE nie podoba mi się ten switch w tej funkcji - powoduje to, że za każdym razem, gdy będę dopisywał nową klasę obiektu, będę musiał (oprócz napisania samej klasy, oraz metod do zapisu/odczytu z/do stringa dla niej) , zmodyfikować także zupełnie inną klasę, gdzie znajduje się definicja funkcji czytającej listę obiektów z pliku. Mam nieodparte wrażenie, że dałoby się całość zorganizować tak, aby wczytywanie było równie "eleganckie", jak zapis.

To dobry znak że Ci się nie podoba, bo faktycznie nie jest zbyt elegancki.

No cóż, bardzo elegancko by było tylko wtedy gdybyś miał jedną wspomnianą skryptowalną klasę Creature. Bez tego trzeba trochę kombinować. Rozwiązania które można wziąć pod uwagę są dwa:

  1. Słownik fabryk. Brzmi to może nietypowo, ale chodzi o coś takiego:
Dictionary<string, Func<ObjectDataCzyCoś, GameObject>> fac;
...
fac["Rat"] = (s) => new GameObject(s);

I teraz żeby stworzyć obiekt którego typ masz w stringu objType, zakładając że dane masz w zmiennej objectData robisz coś takiego

GameObject newObj = fac[objType](objData)

Wada jest taka że trzeba ten słownik i tak gdzieś wypełnić, ale i tak jest to dużo lepsze od switcha...

  1. Refleksja. Możesz stworzyć w runtime obiekt dowolnej klasy, mając tylko jej nazwę w stringu. Wymaga to trochę magii, ale da się - sam coś takiego kiedyś robiłem.

Pytanie numer 3: Czy da się iterować po wszystkich typach (!) dziedziczących z GameObject? (pewnie się nie da, ale nie zaszkodzi spytać)

Również za pomocą refleksji, i również wymaga to trochę magii ;]

Pytanie numer 4: Jak napisać warunek IF, który będzie spełniony, jeżeli zmienna obj jest typu Monster (czyli także Rat lub Dog)?

Nie rób tego... To prawdopodobnie oznacza że masz coś źle z projektem klas.
Ale jeśli musisz...

if (zmienna is Monster) { } 
0
msm napisał(a):

To ja ponarzekam sporo pewnie ;]

Bardzo mi miło, w sumie o to mi chodziło :).
Projekt piszę głównie po to, żeby się czegoś nauczyć w trakcie pisania (właściwie nigdy wcześniej nie pisałem nic, co by wymagało korzystania z dziedziczenia). Niewykluczone, że w ogóle nigdy nie zostanie skończony.

I tak dalej, i tak dalej. Z klasy Creature z kolei dziedziczyć będą później np. klasy Wolf, Rat i Dragon. Ogólnie - tych klas będzie dużo. Chcę, żeby to było rozszerzalne.

Rozszerzalne, to by była jedna, klasa Creature i żadnych klas dziedziczących. Trick polega na tym że dla każdego potwora tworzy się kilka skryptów opisujących zachowanie - ale to dużo roboty, dlatego nie namawiam Cię do implementacji tego ;].

Co do tego, czy z Creeature będzie dziedziczył Rat,Wolf i tak dalej jeszcze tak całkiem pewien nie jestem - ale na pewno GameObject to może być wróg, element mapy, albo przedmiot. Będą więc opisywane przez zupełnie różne parametry, ale wszystkie będą miały wspólne kilka metod (chociażby save(), load(), draw()), i wszystkie będzie można przechowywać we wspólnej liście.

Muszę jeszcze przemyśleć, czy z przedmiotu i wroga będzie się jeszcze dziedziczyć "głębiej", ale przyznam, że kusi mnie to.
Zdaję sobie sprawę jednak sprawę z tego, że konstrukcja w stylu GameObject:InventoryItem:Weapon:Blunt:Mace będzie raczej kłopotliwa w użyciu.

Najbardziej interesują nas w tym momencie metody string save() i void load(string) - chodzi o to, aby zapisać wszystko do pliku. Przyjąłem sobie, że w tym pliku każda linijka odpowiada jednemu obiektowi, i zaczynać się będzie od nazwy typu (oczywiście, można to zmienić).

Nie brzmi to zbyt dobrze - implementując to naiwnie, w każdej klasie będziesz musiał sobie od zera parsować całe dane.
Żeby to się dało dość wygodnie parsować, najlepiej zainwestować w format przechowujący pary klucz:wartość.

W chwili obecnej jedynym obiektem zaimplementowanym w grze jest Door, a plik plik save wygląda następująco:

Door	position: (16,7)	name: drzwi	isLocked: False	isOpen: False	lockId: 0
Door	position: (29,7)	name: drzwi	isLocked: False	isOpen: True	lockId: 0
Door	position: (38,7)	name: drzwi	isLocked: False	isOpen: False	lockId: 0

Poszczególne obiekty w oddzielnych linijkach, w każdej linijce pary nazwa:[spacja]wartość, poszczególne pary oddzielone tabulatorami.
Czyli mam tak, jak mówisz, a i tak w każdej klasie od zera parsuję całe dane... Możesz rozwinąć, w jaki sposób może mi to pomóc?

Da się jeszcze prościej - możesz to zrobić za pomocą serializacji obiektów, wtedy się nie będziesz musiał w ogóle męczyć ;]

Dziękuję, poczytam. Przyznam się szczerze, że choć słowo "serializacja" już gdzieś słyszałem, to nigdy nie zastanawiałem się, co to właściwie jest.

1 użytkowników online, w tym zalogowanych: 0, gości: 1