Jump to content
Unity Insider Forum

Saubere Klassenhierarchie aufbauen


Garzec

Recommended Posts

Guten Abend,

mein Ziel ist eine ordentliche Klassenhierarchie für das Kampfsystem aufzubauen. Ich versuche mich an die Objektorientierung heranzuwagen und möchte es gerne direkt sauber machen, bevor ich den Müll nie wieder entferne.

 

Das Ganze beginnt beim Spieler, der mit seinem Input (Linksklick) die Basisroutine "Attack()" der obersten Klasse "WeaponController" aufruft.

 

In dieser Klasse werden die Waffen, die aktuell selektierte Waffe, der Waffenwechsel, die damit verbundene Waffenposition und eben das Angreifen verwaltet.

 

Vom WeaponController erben dann drei weitere Klassen, der "MeleeController", der "RangeController" und der "ProjectileController"

 

Der MeleeController kümmert sich dann um den eigentlichen Nahkampfangriff und alles was dazugehört.

 

Der RangeController kümmert sich um den Fernkampfangriff und alles was dazugehört.

 

Als dritte Klasse, die eigentlich nur zum Fernkampf gehört, kommt dann der ProjectileController, der dafür zuständig ist die Projektile zu verwalten, Geschwindigkeit, Bewegung etc.

 

Die eigentliche Nahkampfwaffe (zb Schwert) erbt dann vom MeleeController. Beim Schwert sind die Informationen wie Angriffsgeschwindigkeit, Reichweite etc. hinterlegt. Die Informationen werden dann in den Basisklassen abgerufen.

 

Ebenso erbt der Bogen vom RangeController und alle Projektile vom ProjectileController.

 

Zu meiner Frage, ist das Ganze zu viel des Guten? Geht es auch eleganter? Ich dachte mir, mithilfe dieser Basisklassen wäre es später möglich, einfach neue Waffen hinzufügen zu können, die dann von den Basisklassen problemlos genutzt werden.

 

Danke für Antworten und Tipps :)

Link zu diesem Kommentar
Auf anderen Seiten teilen

Bevor du dich da reinsteigerst: Klassenhierarchien sind in der Spieleentwicklung ziemlich problematisch.

Unity gibt dir nicht umsonst ein Entity-Component-System - es ist einfach fast immer besser.

Statt "Masterschwert erbt von Schwert erbt von Nahkampfwaffe erbt von Waffe erbt von Pickup" nimmst du ein GameObject und gibst ihm eine Pickup-Komponente, eine Waffen-Komponente, eine Nahkampf-Komponente und eine Masterschwert-Komponente.

 

Darüber kann man sicherlich diskutieren, aber ich finde, wenn du in Unity mehr als eine Vererbungsstufe unter MonoBehaviour bist, dann machst du etwas falsch.

Manchmal wirkt die ECS-Variante etwas unintuitiver als eine Klassenhierarchie, aber die Vorteile sprechen für sich.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Interessant, ich bin zwar nicht Threadersteller, ich erlaube mir aber mal zu dem Thema nach einer Meinung zu fragen, da mich das auch sehr interessiert und es durchaus auch dem Threadersteller weiterhelfen kann.

 

Ich habe mich bisher immer an Büchern zu C#(nicht zu Unity) gehalten, da mein Wissen leider sehr bruchstückhaft aus dem Internet und Büchern ist. Bisher habe ich immer alles mit Vererbung gemacht, mein Inventarsystem sieht zum Beispiel so aus, könnte ich dazu mal eine Meinung/Verbesserungsvorschlag haben?

 

Item.cs

using System.Collections;
using UnityEngine;
public abstract class Item
{
//Name des Items
public string name;
//Icon des Items
public Sprite icon;
}

ItemStack.cs

public class ItemStack {
//Typ des ItemStacks
public Item item;
//Menge des Items im Stack
public int amount;
public ItemStack(Item newItem, int itemAmount = 1)
{
	item = newItem;
	amount = itemAmount;
}
}

Weapon.cs

using System.Collections;
public abstract class Weapon : Item
{
}

Axe.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Axe : Weapon
{
private static Axe _instance;
public static Axe instance
{
	get
	{
		if(_instance == null)
		{
			_instance = new Axe();
		}
		return _instance;
	}
}
public Axe()
{
	name = "Sword";
	icon = Resources.Load<Sprite>("icon_axe3");
}
}

Slot.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Slot
{
public ItemStack containedItem;

public bool IsEmpty()
{
	if (containedItem == null)
		return true;
	return false;
}
}

Inventar.cs

public class Inventar
{
//Alle item slots dieses Inventars
public Slot[] slots;
....

Eine Itemliste im Editor zum erstellen der Items habe ich gar nicht, jedes Item bekommt ein neues Script, da ich bisher die Erfahrung gemacht habe, dass wenn ich die Items im Editor(public List<Item>...) und nicht per Script erstelle, es dann problematisch wird bei Quests, Loot etc., da man dort dann z.B. den Index des Items(items.Get(x)) kennen muss, während man so z.B. Axe.instance als Singleton verwenden kann.

 

Wie würdest du es mit dem Komponentsystem machen?

Link zu diesem Kommentar
Auf anderen Seiten teilen

(Garzec: Wenn dir diese Posts nicht weiterhelfen, sag Bescheid. Dann verschiebe ich sie.)

 

Es gibt für alles Anwendungsfälle. Wenn dein Spiel ist wie meisten Zeldas oder RAGE, der Spieler also jede Waffe irgendwann ein Mal bekommt, dann kann Singleton für Waffenklassen sinnvoll sein.

Ein anderer Fall fällt mir gerade nicht ein, in dem ein Singleton nicht irgendwie kontraproduktiv wäre.

 

An was für Stellen brauchst du denn Axe.instance? Du brauchst ja Code, in dem du Axe.instance referenzierst, und dieser Code muss am Ende auch wieder in die Szene gezogen werden. Warum also nicht die Axt selbst in die Szene ziehen?

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ja, jedes Item soll benutzbar vom Spieler sein....

 

Momentan brauche ich die Instance noch nicht, aber wenn ich dann später ein Quest System schreibe, dann ist es finde ich besser das so zu haben:

QuestSystem.medium.AddLoot(new ItemStack(Axe.instance, 1));

als

QuestSystem.medium.AddLoot(new ItemStack(Inventar.itemList.Get(54), 1));

 

Wie würdest du die Axt in die Szene ziehen? Mir ist das etwas unklar, wie man das nur mit Komponenten realisieren kann ^^

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ja, jedes Item soll benutzbar vom Spieler sein....

Davon habe ich nichts gesagt - es ging darum, wie oft etwas vorkommt :)

 

Der Trick ist, dass du deine Quests auch nicht mit Code schreibst.

Man kann den Prozess der Spieleentwicklung ja (unter anderem) in zwei Teile teilen: Die Programmier- und die Designarbeit.

Ich finde es immer sehr gut, wenn die Designarbeit nicht in Code stattfindet. Wenn du also ein Quest designst, solltest du immediate Feedback * haben oder optimalerweise einen Designer dransetzen können, ohne dass dieser Coden können muss.

 

* In dem Video geht es eigentlich um etwas ganz anderes, aber der Teil über Immediate Feedback ist großartig. Eigentlich ist der ganze Vortrag erstklassig, einfach mal ansehen.

 

Ich habe bisher noch keine Designaufgabe gefunden, für die man in Unity nicht eine Code-lose Lösung implementieren könnte. Sprich: Du schreibst einen Haufen (teilweise Editor-)Code, und wenn der erstmal läuft, kannst du deine Quests alle ohne weiteren Code bauen. Ebenso deine Items. Und deine NPCs. Und deine Dialoge.

Dann hast du auch wesentlich weniger Code zum Testen... und das Entwickeln macht mehr Spaß :)

 

Für Waffen, wie für alle Items, würde ich entweder zu Prefabs oder zu ScriptableObjects greifen. In einigen Fällen, oft bei Waffen, ist eine Kombination von beiden sinnvoll. Beide werden in den Assets angelegt und können nach Belieben in die Szene gezogen werden.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Also ich habe mal die Klassen auf das Wesentliche versucht runterzukürzen. Ich kann ja mal zeigen, wie ich es momentan geregelt habe.

 

Im WeaponController sind erstmal (kann ich ja noch auslagern) alle Routinen drin.

 

GameObject currentSelectedWeapon;
public GameObject[] weapons;
public Vector3 WeaponPosition { get; set; }
public Quaternion WeaponRotation { get; set; }
public GameObject WeaponProjectile { get; set; }
public Transform ProjectileSpawnPoint { get; set; }
public int ProjectileSpeed { get; set; }
public int AttackRate { get; set; }
public int MaxAmmo { get; set; }
int currentAmmo;
private void Start()
{
	SelectWeapon(0);
}
public void Attack()
{
	/*
	if ()
	{
		StartRange();
	}
	else
	{
		StartMelee();
	}
	*/
}
private void StartMelee()
{
}
private void StartRange()
{
	LaunchProjectile();
	SetProjectileMovement();
}
private void LaunchProjectile()
{
	Instantiate(WeaponProjectile, ProjectileSpawnPoint.position, ProjectileSpawnPoint.rotation);
}
private void SetProjectileMovement()
{
	WeaponProjectile.transform.Translate(Vector3.forward * ProjectileSpeed * Time.deltaTime);
}
private void SelectWeapon(int weaponIndex)
{
	foreach (GameObject weapon in weapons)
	{
		weapon.SetActive(false);
	}
	weapons[weaponIndex].SetActive(true);
	currentSelectedWeapon = weapons[weaponIndex];
	currentSelectedWeapon.transform.position = WeaponPosition;
	currentSelectedWeapon.transform.rotation = WeaponRotation;
}

 

Dazu gibt es dann nur noch jede einzelne Waffe/Projektil

 

public class BowController : WeaponController
{
public Transform weaponPosition;
public GameObject bowProjectile;
public Transform projectileSpawnPoint;
private void Start()
{
	WeaponPosition = weaponPosition.position;
	WeaponRotation = gameObject.transform.rotation;
	WeaponProjectile = bowProjectile;
	ProjectileSpawnPoint = projectileSpawnPoint;
	AttackRate = 3;
	MaxAmmo = 30;
}
}

 

public class ArrowController : WeaponController
{
private void Start()
{
	ProjectileSpeed = 10;
}
}

 

public class SwordController : WeaponController
{
public Transform weaponPosition;
private void Start()
{
	WeaponPosition = weaponPosition.position;
	WeaponRotation = gameObject.transform.rotation;
	WeaponProjectile = null;
	AttackRate = 3;
}
}

 

Ich hoffe, damit zu erreichen, dass ich bei jeder Waffe nur die Stats etc. hinterlegen muss, damit die Basisklasse den Rest erledigt. Um die Daten zu verwalten, da ich ja nur Anfänger bin, hoffte ich auf magische Wörter wie "override". Das sollte ich mir mal anschauen, ich glaube damit kann ich dann die gelieferten Daten immer passend überschreiben. Die Start Methoden sind erstmal nur Dummy Methoden.

 

So war zumindest mein Plan :D

Link zu diesem Kommentar
Auf anderen Seiten teilen

Omg, Danke :D

 

Ich weiß, dass ich in meiner Zeit als ich mit Unity angefangen habe, was vor 4 und einem halbem Jahr war, und noch Copy&Pasted hatte, ein ScriptableObject bei BurgZerg Arcade benutzt hatte, aber leider nicht gewusst, was ich da eigentlich mache, hätte ich es verstanden, hätte es mir viel Arbeit gespart

 

Ich suche ungelogen seit 3-4 Jahren nach einer Möglichkeit Items im Editor zu erstellen, aber keine ItemID's bei Dialog, Quest, Loot zu verwenden, ich habe auf meinen Festplatten alleine 5 Projekte mit verschiedenen Inventarsystemen, mit Singleton, Enums, Class, System.Typ....

 

Ich bin dann mal weg und schreibe mein Inventarsystem neu :)

 

Edit: So, noch ein EditorWindow dazu programmiert :)

7xDelEx.png

Link zu diesem Kommentar
Auf anderen Seiten teilen

[...] "override". Das sollte ich mir mal anschauen, ich glaube damit kann ich dann die gelieferten Daten immer passend überschreiben. Die Start Methoden sind erstmal nur Dummy Methoden.

Wenn du eine Klassenhierarchie bauen willst, dann ist das schon ein richtig klingender Weg. Allerdings würde ich nach wie vor dazu raten, so wenige und so flache Klassenhierarchien wie möglich zu bauen.

In deinem gekürzten Beispiel kommt die Definition von WeaponController gar nicht drin vor. Ich weiß jetzt nicht einmal, warum deine drei aufgelisteten Controller überhaupt davon erben.

 

Aber ich sehe schon das Problem. An irgendeinem Punkt in deinem Code wird abgefragt, ob der Spieler "angreifen" drückt und der Impuls dieses Inputs muss irgendwie zur Waffe gelangen. Da die Waffen unterschiedlich funktionieren, muss letztenendes unterschiedlicher Code ausgeführt werden. Man muss also von diesem Input-Startpunkt aus irgendwie in unterschiedliche Implementationen hineinrutschen.

Das ist schon genau das, wofür Klassenhierarchien da sind.

AbstractWeapon weapon = new WeaponSword();
weapon.Attack(); // und dann mal gucken was passiert

Und ich denke auch, dass ein Vererbungsschritt okay ist. Also WeaponSword erbt von AbstractWeapon erbt von MonoBehaviour.

Eklig wird es meiner Meinung nach, wenn man da noch eine oder mehr Ebenen draufpackt. Für Fälle, in denen man sich dazu gezwungen sieht, gibt es alternative Möglichkeiten, die auf Delegation statt Vererbung setzen - und eben auf Unitys Komponentensystem.

 

Trotzdem eine kurze Zusammenfassung zu Vererbungs-Keywords:

virtual gibt an, dass die Methode von einer erbenden Klasse überschrieben werden darf.

public virtual void Foo()
{
 DoSomething();
}

 

abstract gibt das auch an, aber außerdem, dass es auf dieser Ebene keine sinnvolle Implementation für die Methode gibt.

public abstract void Foo();

Ein Beispiel für eine abstrakte Methode wäre Attack() der Basisklasse der Waffen. Es erben Schwerter, Bögen und Äxte von dieser Klasse, aber was für eine Waffenart soll "Waffe" selbst sein?

Wir reden dabei von "abstrakten" Konzept einer "Waffe". Und ein solches Konzept kann man nicht in der Hand halten, geschweige denn damit Attack()-ieren.

Wenn eine Methode als abstract markiert wurde, muss die ganze Klasse ebenfalls abstract sein:

public abstract class Weapon : MonoBehaviour
{
 // ...
}

Das bewirkt, dass man keine Instanzen mehr von "Weapon" erzeugen kann, es also auch nicht mehr auf GameObjects ziehen kann. Das spiegelt genau wieder, was ich eben meinte: Man kann das abstrakte Konzept einer Waffe nicht in der Hand halten.

 

Mit override überschreibst du dann eine virtuelle oder abstrakte Methode. Wenn du von einer abstrakten Klasse erbst, musst du alle abstrakten Methoden überschreiben. Sonst wüsste das Programm bei deiner Axt immer noch nicht, was Attack() bei deiner Axt bedeutet. Und um eine Methode auszuführen, muss sie ja irgendwo implementiert sein.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Also so wie Sascha das schon beschrieben hat mit den Componenten mache ich auch. Ich lasse zwar vieles erben, aber je nach dem.

Beispiel Entity <- Player | Enemy

Aber sowas we HP und so weiter mache ich manchmal Seperate. HealthSystem oder DamageSystem. Später kann man sogar abfragen, ob so ein Komponent existiert, wenn nicht, dann bekommt das Ding einfach kein Damage. Wiederum frag ich auch da ab, ob man einen TeamScript hat usw. Ist schon echt Klasse mit den Komponenten zu arbeiten.

 

EDIT: Was ich noch lustig finde es, dass man nach interfacen als Component suchen kann. Beispiel GetComponent<IDamageAble>(). Wenn es existiert dann gib ihn faust.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Was auch noch etwas gegen Vererbungen spricht, ist die Tatsache, dass du mit zunehmenden Eigenschaften unglaublich viele Basisklassen hast, weil C# nur von einer Klasse erben kann. Als Beispiel:

 

Waffe
|----Nahkampf
|	 +----Nahkampf mit Heilung
|	 +----Nahkampf mit Magie
|	 +----Nahkampf mit Magie und Heilung
+---Fernkampf
  +----Fernkampf mit Heilung
  +----Fernkampf mit Magie
  +----Fernkampf mit Magie und Heilung

 

Da sind Komponenten, wie Sascha sagte, wesentlich flexibler und einfacher.

 

Waffe - NahkampfKomponente - MagieKomponente - HeilungKomponente

 

Auch in der Programmierung von Geschäftsanwendungen wird schon länger von ausufernder Vererbung abgeraten. Hier geht der Trend genauso zu Komponentisierung (z.B. Microservices).

Link zu diesem Kommentar
Auf anderen Seiten teilen

dass du mit zunehmenden Eigenschaften unglaublich viele Basisklassen hast, weil C# nur von einer Klasse erben kann

 

@Hrungdak: Will dir nicht widersprechen sondern mein eigenes Verständnis erweitern. Gegen die Tatsache dass C# nur von einer Klasse erben kann könnte man doch Interfaces verwenden oder bin ich da ganz am Thema vorbei?

 

Also natürlich nur wenn man den Lösungsweg mit Komponenten nicht gehen will.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Interfaces können halt keinen Code beinhalten. Es stimmt schon, dass damit einige Sachen möglich sind, aber in Hrungdaks (gutem) Beispiel müsstest du Heilung in einem Interface zwar nur ein Mal deklarieren, aber dann doch wieder zweimal (gleich) implementieren.

 

Abgesehen davon bekommt man bei Interfaces ein etwas gruseliges Problem, das auch bei Mehrfachvererbung entstehen würde: Es macht Unitys Black Magic kaputt. Beispiel:

private IHeilung heilung; // Das Interface, das meine Komponente hat
public HeilungFernkampf hf; // Hier ist eine Komponente reingezogen, die IHeilung implementiert

void Awake()
{
 heilung = (IHeilung)hf;
 Destroy(hf);
}

void Update()
{
 if(heilung != null)
 {
   print("Das Ding ist ja immer noch da");
   heilung.Foo();
 }
}

Ich glaube, ich muss nicht viel dazu sagen :)

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...