Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

KeksX

Community-Fossil

  • »KeksX« ist der Autor dieses Themas

Beiträge: 2 107

Beruf: Game Designer

  • Private Nachricht senden

1

30.07.2015, 14:29

[Unity][C#] RTS Unit Logik - Variable Upgrades und veränderbares Verhalten optimal implementieren

Moin moin,

ich arbeite derzeit an einem kleinen Unity Singleplayer RTS mit, bei dem ich unter anderem an der Kampf- bzw. Unitlogik arbeite. Es haben sich ein paar Änderungen ergeben, bei denen ich direkt das ganze System optimieren möchte. Und da sich ja doch ein paar Experten hier tummeln, möchte ich mal die Frage in den Raum werfen.

Aktueller Zustand
Ich habe Kampfgruppen(man stelle sich Total War stark vereinfacht vor). Über "Fähnchen" steuere ich meine Einheiten, die dann mit Hilfe einer sehr simplen KI darauf reagieren: Zum Punkt hinlaufen & verteidigen, in die Basis rennen und dort einlagern usw. Verschiedene Prioritäten sind im Kampf möglich, also was zuerst angegriffen wird, sowie verschiedene Commands(Attack Move, Normal Move etc).
Diese Einheiten haben ganz normal Attribute wie Angriffsstärke, Bewegungsgeschwindigkeit usw.

Jede Einheit hat genau eine Funktion zum Schaden austeilen und Schaden erhalten(wird über Animationen gesteuert), es gibt lediglich Schaden und ggf. noch Rüstung(je nach Unit).

Also Pseudo Code(im echten Code ist es etwas komplexer, eine Basisklasse für Units mit Standardverhalten sowie für selektierbare Objekte usw) für eine Unit Class wäre jetzt

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class FriendlyUnit  {

void Update() {
       checkCommands(); 
       // Sonstiges Unit Gedöhns
}


// wird vom Animator aufgerufen
void attack() {
     currentTarget.SendMessage("receiveDamage", unitDamage);
}

void receiveDamage(int damage) {
     unitHealth -= damage;
}

void checkCommands() {
    switch(unitcurrentCommand) {
         case(Commands.Attack):
       // unit-spezifisches Verhalten
       // ...
    }
    // ...
}

// ...



Simpelster Natur und in meinen Augen noch prototypsich da bis jetzt noch keine Änderungen für das System kamen. Bis jetzt halt.

Folgende Änderungen sollen nun auftauchen
Upgrades, die Attributswerte erhöhen(trivial und bereits implementiert).

Upgrades, die Verhalten und Standardwerte kontextsensitiv erhöhen.
Das können sein ist aber nicht beschränkt auf:
- Ein Schild, das Schaden absorbiert
- Attributeswerte, die sich erhöhen, wenn ein gewisser Einheitentyp in der Nähe ist
- Einen Effekt, der sich auf umliegende Einheiten auswirkt(z.B. sowas wie eine Heil-Aura)

Es können also die verschiedensten Verhaltensänderungen kommen, die sich auf verschiedene Bereiche auswirken. Selbiges gilt auch für verschiedene Einheitentypen (speziell Gegner, Gegner Typ A verhält sich anders als Gegner Typ B. Das ist aktuell auch provisorisch über Vererbung gelöst, die Einheiten haben einfach andere Funktionen für die verschiedenen Commands und andere Prioritätslisten).

Ich habe zwar einen gewissen Zeitdruck, weigere mich aber ehrlich gesagt jetzt die schnellste und einfachste Möglichkeit zu wählen(z.B. alles wie bis jetzt einfach hardcoden), da ich gerne den Code wiederverwenden würde. Also würde ich gerne die gesamte Unit Logik etwas überarbeiten und ggf. komplett neu aufziehen. Ich denke die Zeit habe ich auch, besonders da Urlaub vor der Tür steht.

Mein aktueller (halbfertiger) Gedankengang ist der, dass ich quasi für jede Verhaltensart (also UnitDealDamage, UnitReceiveDamage, UnitStandAround, UnitInFight, UnitOutsideFight usw) Delegates verwende, und den mit Hilfe vom Unity Component-System fülle. So wäre ein Upgrade, das Schild absorbiert, eine einfache Addierung zu diesem Delegate. Wenn z.B. ein Upgrade bereits vorhandenes Verhalten überschreibt(z.B. das Schild), ziehe ich entsprechende Delegates wieder ab.

Sicher, ob das so funktionieren würde und effektiv ist, bin ich mir aber nicht. Hat jemand vielleicht schonmal ein solches System umgesetzt oder hat Ideen, wie man das Ganze schöner lösen könnte?

Würde mich über Hilfe freuen!
WIP Website: kevinheese.de

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »KeksX« (30.07.2015, 14:37)


KeksX

Community-Fossil

  • »KeksX« ist der Autor dieses Themas

Beiträge: 2 107

Beruf: Game Designer

  • Private Nachricht senden

2

01.08.2015, 22:47

Kurzes Update:

Ich sitze aktuell an einer eigenen Lösung, die (wahrscheinlich) ohne Delegates klarkommt und etwas einfacher für den Game Designer sein wird. Bin immernoch für jeden Input zu haben, werde aber meine Lösung für das Problem posten sobald sie fertig ist.
WIP Website: kevinheese.de

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

3

02.08.2015, 14:46

Das klingt alles nach einem Regelwerk für das erhalten und Austeilen von Schaden. Letztendlich lässt sich das vielleicht so lösen, in dem du deiner Basisklasse Properties für die jweiligen Attribute verpasst (Strength, Defense, usw.) und dann gibt es eine statische Klasse, die den Schaden dafür ausrechnet. Dafür greift sie dann auf die Properties der Objekte zu. Damit du es später leichter in deiner Vererbungshierarchie hast, sollten die für die Schadensberechnug benötigten Daten in einem Interface definiert sein, dann kannst du das einfach jede Implementierung implementieren lassen. Dann sagst du einfach Einheit 1 greift Einheit B an, die Klasse ermittelt dir den Schaden (oder ein ResultObjekt, falls du komplexere Ergebnisse eines Angriffes hast) und den sendest du an den Gegner.

KeksX

Community-Fossil

  • »KeksX« ist der Autor dieses Themas

Beiträge: 2 107

Beruf: Game Designer

  • Private Nachricht senden

4

02.08.2015, 15:52

An so etwas in der Richtung habe ich auch schon gedacht.

Es gäbe quasi ein Interface für alles, was "Teil des Kampfsystemes" ist. Dies beinhaltet properties für alle möglichen(nicht immer verwendeten) Attribute sowie grundlegende Methoden zum erhalten/austeilen von Schaden. Jedes Objekt würde dann für sich selbst entscheiden, was es tut, wenn es bspw. Leben <= 0 hat.

Upgrades, die jenseits von Statusveränderungen sind, wären dann "Abilities". Also Scripts, die man einfach den Objekten hinzufügen kann und auf das Interface des eigenen Objektes zugreifen, um gewisse Effekte(wie bspw Heilaura: Für jede Unit der gleichen Fraktion in Range[Liste dafür im Interface] wird alle X Sekunden aktuelles Leben um Y erhöht) zu erzeugen. So müsste ich weder mit Delegates arbeiten noch irgendwo, abseits von ggf. einem Counter, die Daten dieser Abilities speichern. Sie wären einfach nur Eigenschaften des Objekts.

Die statische Klasse zur Schadensberechnung ist eine gute Idee. Vor allem könnte ich dort alle möglichen Sonderregelungen implementieren. Vielen Dank für den Tipp!


(Bin aber weiterhin für jeden Input offen :) )
WIP Website: kevinheese.de

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

5

02.08.2015, 21:14

Da ich ein Fan von UML bin, habe ich das Bild angehangen.


Unit ist das Interface mit Eigenschaften. Effect ist das Interface für einen Effekt auf eine Einheit. Diese werden alle regelmäßig aktualisiert und klingen ggf. ab. Man könnte die Werte, die der Effekt auf die Einheit hat, in dem Property der Einheit dazu berechnen.

Ansonsten halt die statische Klasse, ich habe sie jetzt mal AttackDealer genannt, der das ganze berechnet und ein Ergebnisobjekt zurückliefert. Dort ist dann der Schaden und ggf. resultierende Effekte (Vergiftung, Vereisung für die Zieleinheit dabei).

Die Interfaces sind nicht vollständig natürlich. Mir geht es nur um die Typen und deren Beziehung zueinander. Finde ich 100 mal sprechender als eine technische Beschreibung. Damit sollten sich alle Anwendungsfälle lösen lassen.
»TrommlBomml« hat folgendes Bild angehängt:
  • uml.png

KeksX

Community-Fossil

  • »KeksX« ist der Autor dieses Themas

Beiträge: 2 107

Beruf: Game Designer

  • Private Nachricht senden

6

02.08.2015, 21:28

Ich hatte gerade MagicDraw ausgepackt, da ich dafür noch einige Wochen eine Studilizenz habe. :thumbsup:

Ich halte den Ansatz auf jeden Fall für sinnvoll. Ich werde ihn implementieren und, falls Interesse besteht, gerne auch den Code dafür posten!
WIP Website: kevinheese.de

KeksX

Community-Fossil

  • »KeksX« ist der Autor dieses Themas

Beiträge: 2 107

Beruf: Game Designer

  • Private Nachricht senden

7

16.08.2015, 18:52

Hallo,

ich weiß nicht ob es jemanden interessiert, aber da ich ein Fan von "Ich habe ein Problem gelöst und hier ist die Lösung für alle anderen" bin(Ja, so nenne ich das jetzt), poste ich hier mal meine Lösung. Es handelt sich hierbei um die erste Iteration, die ich aktuell leider nicht verbessern kann, da ich etwas an Zeitmangel leide.

Hier ist, was ich getan habe:

1) Nach dem Vorbild von Unity habe ich alles in einzelne Komponenten ausgelagert. Für jedes Verhalten gibt es eine entsprechende Komponenten. Da GameObject.GetComponent<>() zu Performanceproblemen führt, habe ich außerdem eine Klasse geschrieben, die das alles zusammenhält. Das Ganze habe ich mal eben hier veranschaulicht:


(Link)


Natürlich gibt es hier entsprechende Schnittsellen, damit die Kommunikation zwischen den Klassen geregelt ist. Hier ist nur beispielhaft aufgelistet, was die Scripts beinhalten. Ist schneller, als sie mit Code aufzulisten.

2) Ich habe ein ScriptableObject erstellt, das ein "SkillResult" darstellen soll. Dies ist die Grundlage des Systems. Jedesmal, wenn eine Einheit einen Effekt auf ein anderes Objekt wirken möchte, muss sie ein solches SkillResult erstellen.

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class SkillResult : ScriptableObject {
    // enums

    public enum SkillTargetType {
        Unit,
        UnitGroup,
        None
    }

    public enum SkillType {
        Damage,
        Heal,
        Buff,
        Debuff,
        RemoveBuff,
        Other
    }

    public enum SkillAttribute {
        AttackRange,
        AttackSpeed,
        AttackDamage,
        HealthRegeneration,
        ShieldRegenration,
        MovementSpeed,
        VisionRange,
        Health,
        Shield,
        None
    }

    public enum SkillPowerType {
        Absolute,
        Percantage
    }

    // Flags
    public static string flag_recentlyHealed = "RecentlyHealed";

    // Skill Data
    public SkillType skillType = SkillType.Heal;
    public SkillAttribute skillAttribute = SkillAttribute.Health;
    public SkillPowerType skillPowerType = SkillPowerType.Absolute;

    public string skillFlag = "";
    public string skillFlagUnique = "";
    public float skillFlagTimer = 0.0f;

    public float skillPower = 0.0f;
}


Hier werden alle grundlegenden Werte eines Skills gespeichert. Typ, auf was/wen er sich auswirkt, welchen Effekt, welches Attribut, wie stark, wie lang der Effekt bleibt usw.

Dieses SkillResult wird an den SkillCalculator weitergegeben. Dies ist nichts anderes, als eine Art Mittelsmann, der überprüft, ob Skills gewirkt wirken können, ob Effekte in Kraft treten usw. Vereinfacht gesagt, wenn ich davon ausgehe, dass der Skill in der Unit 1:1 so übernommen werden soll, sähe das so aus:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
public class SkillCalculator : MonoBehaviour {

    public static void passSkillResult(GameObject attacker, GameObject target, SkillResult skillResult) {
        if(target != null) {
            target.SendMessage("OnSkillResult", skillResult, SendMessageOptions.DontRequireReceiver);
        }
        
    }

}


Hier könnte ich allerdings alle möglichen Sonderregeln einführen, wie im vorherigen Post bereits angedeutet. "Wenn das Ziel Effekt X hat, dann wirkt der Skill Y anders".
Ansonsten hat jedes Skript, das von einem Skill getroffen werden kann, eine Funktion "onTakeSkillResult". Für das HealthScript sähe das beispielsweise so aus:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    void OnSkillResult(SkillResult skillresult) {
        if(skillresult.skillType == SkillResult.SkillType.Damage) {
            float removedFromShield = 0;

            if(CurrentShield > 0) {
                removedFromShield = Mathf.Abs(CurrentShield - skillresult.skillPower);
            }

            CurrentHealth = Mathf.Clamp(CurrentHealth - (skillresult.skillPower - removedFromShield), -1.0f, BaseHealth);
        }

        else if(skillresult.skillType == SkillResult.SkillType.Heal) {
            CurrentHealth = Mathf.Clamp(CurrentHealth + skillresult.skillPower, -1.0f, BaseHealth);
        }

        else if(skillresult.skillType == SkillResult.SkillType.Buff) {
            if(skillresult.skillAttribute == SkillResult.SkillAttribute.Shield) {
                BaseShield += skillresult.skillPower;
                CurrentShield = BaseShield;
            }

            if(skillresult.skillAttribute == SkillResult.SkillAttribute.Health) {
                BaseHealth += skillresult.skillPower;
                CurrentHealth = BaseHealth;
            }
        }

        checkDeath();
    } 



Zuerst wird überprüft, um welchen Skilltypen es sich handelt. Dann wird überprüft, welches Attribut es betrifft, wenn es sich bspw um einen Buff handelt, und entsprechende Werte überprüft.
Da es hier zum Tode eines Objekts kommen kann, wird dies sofort überprüft.

Zu guter Letzt die Skills an sich. Um das Entfernen/Hinzufügen von Skills zu vereinfachen, sind auch diese Komponentenbasiert. Jeder Skill besteht aus 3 Elementen:


1) Dem Skill Script. Dieses sagt dem Skillmanager und anderen Elementen was der Skill kostet, auf was er sich auswirkt usw. Außerdem ist er gleichzeitig die Schnittstelle für die weiteren Elemente des Skills.

Vereinfacht sieht das so aus:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Skill : MonoBehaviour {

    public int skillType { get; set; } 
    public UnitScript unitScript;
    public UnitGroupScript unitGroupScript;

    public int researchTime { get; set }
    public int costA_research { get; set}
    public int costB_research { get; set }

    public int costA_equip { get; set; }
    public int costB_equip { get; set;}

    public int manaCost { get; set; }
}


Nun gibt es mindestens zwei weitere Skripts für jeden Skill:
1) Wann wird der Skill ausgelöst?

2) Was macht der Skill, wenn er ausgelöst wird?

Beispielsweise habe ich hier einen Heal Skill, der alle 10 Sekunden 5 befreundete Einheiten in der Nähe um x-Punkt heilt.

Ersteinmal das "Cast Script", das bestimmt, wann der Skill ausgelöst wird:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AutomaticCast : MonoBehaviour {

    public float castCooldown;
    private float nextCast;
    // Use this for initialization
    void Start () {
        nextCast = Time.time + castCooldown;
    }
    
    // Update is called once per frame
    void FixedUpdate () {
        if(Time.time > nextCast) {
            SendMessage("OnFire");
            nextCast = Time.time + castCooldown;
        }
    }
}


Sollte selbsterklärend sein. OnFire wird jetzt in einem weiteren Script implementiert:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class AreaHeal : MonoBehaviour {
    public Skill skill;

    public float healpower;
    public float healrange;
    
    public float flagCooldown;

    public UnitFaction targetFaction;
    public int numTargets;

    public void Start() {
        skill = GetComponent<Skill>();
    }

    void OnFire() {
        List<UnitScript> targetUnits = new List<UnitScript>();

        // alle möglichen TargetUnits durchgehen. Hier alle Units innerhalb der Gruppenreichweite, etwas länger darum rausgeschnitten

                // das Script regelt seine Regeln selbst: die Einheiten mit geringster Höhe werden bevorzugt
        sortByLowestHealth(ref targetUnits); 
                // Das SkillResult wird erstellt
        SkillResult skillResult = new SkillResult();
        
        skillResult.skillType = SkillResult.SkillType.Heal;
        skillResult.skillPower = healpower;
        skillResult.skillFlag = SkillResult.flag_recentlyHealed;
        skillResult.skillFlagTimer = Time.time + flagCooldown;
                // Und dann 1:1 an die entsprechende Anzahl Targets gesendet
        for(int i = 0; i < numTargets; i++) {
            SkillCalculator.passSkillResult(this.gameObject, targetUnits[i].gameObject, skillResult);
        }
    }

    void sortByLowestHealth(ref List<UnitScript> list) {
            list = list.OrderBy(x=>x.healthScript.CurrentHealth).ToList();
    }
}



Das wars. Mit diesem System kann ich jeden Skill implementieren und alles was ich tun muss, um einer Unit/UnitGruppe diesen Skill zu geben, ist sein GameObject anzuhängen. Den Rest regelt der Skill selbst und dank SkillManager/SkillCalculator ist auch eine Kommunikation zwischen Skills möglich.
Das ist sicher nicht der schönste Code, das gebe ich offen zu. Allerdings fehlt mir leider die Zeit ihn weiter zu optimieren, was ich allerdings tun werde sobald ich sie habe.

Ich hoffe ich konnte jemanden damit helfen und danke an TrommlBomml :)




P.S:

Die ganzen publics sind natürlich so in dem Code nicht vorhanden. Ich benutze folgende Technik, um Elemente im Inspector anzeigen/ändern zu lassen und dann im Code zu sichern:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
[SerializeField]
float irgendeinWert = 0.0f;

// ....

public float irgendeinWert 
    {
        get
        {
            return irgendeinWert;
        }
    }
WIP Website: kevinheese.de

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »KeksX« (16.08.2015, 19:00)


Werbeanzeige