Jump to content
Unity Insider Forum

RPG: Charakter Stat/Skill Grundsystem


Mark

Recommended Posts

Ich möchte euch ein etwas größeres Snippet für RPG Stats und Modifikationen (Items, buffs, etc) vorstellen.

 

Das ganze ist ein Grundgerüst und es muss darauf aufbauend von der Nutzerseite noch etwas gemacht werden, liegt aber daran dass ich unmöglich alle Bereiche die man haben möchte abdecken kann.

 

Was das System aber so in dieser Grundversion schon kann ist:

- Beleibige Charakterstats/attribute darstellen, sei es ein Float Attribut, Int, Bool, String oder was anderes.

- Stats können verändert werden, sei es permanent oder temporär

- Stat Veränderungen können beliebig rückgängig gemacht werden (Buff verfällt, Item wird abgelegt)

- Stat Veränderungen und Stats sind so transparent gestaltet dass man eine beliebige UI drauf ansetzen kann um dem Spieler alle wichtigen Infos zu zeigen

- Stat Veränderungen und Veränderungen an den Veränderungen können durch Events verarbeitet werden.

 

Zuerst etwas Zucker, wie man das ganze in einer simplen Art und Weise verwenden kann:

 

var health = new Stat<float>("Health", 100);
Debug.Log(health.Value); // 100 HP

// HP reduzieren
var baseModification = (IStatModification<float>)health.Modifications.First();
baseModification.Value -= 10; // leben um 10 HP reduzieren

Debug.Log(health.Value); // 90 HP

// HP um 50 HP boosten, zB durch ein Item
var healthBuff = new StatModification<float>("HealthBuff", 0, new RPG.Stats.Accumulators.Add(), 50);
healthBuff.Attach(health);

Debug.Log(health.Value); // 140 HP

// Buff wieder entfernen
healthBuff.Detach();

Debug.Log(health.Value); // 90 HP

// Auf Veränderungen reagieren
health.Modified += (sender, arguments) =>
{
var floatArguments = (StatModifiedEventArgs<float>)arguments;
if (floatArguments.OldValue < floatArguments.NewValue)
	Debug.Log("Ich wurde geheilt!");
else
	Debug.Log("Ich wurde verletzt!");
};

baseModification.Value += 20; // Ich wurde geheilt!
baseModification.Value -= 10; // Ich wurde verletzt!
healthBuff.Attach(health); // Ich wurde geheilt!

 

Stats und StatModifications haben eine ID die eindeutig sein sollte, aber vom System selbst nicht weiter verwendet wird.

Durch die ID können extra Informationen von einem Drittsystem abgerufen werden um zB Beschreibungstexte zu den einzelnen Elementen anzuzeigen.

 

Stats können wie man sehen kann durch Modifikationen mit Werten befüllt werden, also auch der Grundwert ist eine Modifikation welche wie im Health Beispiel oben implizit erstellt wird.

 

Die Modifikationen können eine Priorität haben (die Grundmodifikation hat eine Priorität die dafür sorgt dass diese immer zuerst angewandt wird)

Prioritäten dienen dazu die Stackmechaniken zu managen, so kann zB dafür gesorgt werden dass alle Wertveränderungen die durch Items verursacht werden (Rüstungsplus, etc) vor Wertveränderungen angewandt werden die durch Verzauberungen kommen (+50% HP).

 

Modifikationen können die Stats dabei auf unterschiedlichste Art und Weise beeinflussen. Im System selbst gibt es 4 Möglichkeiten:

- setzen des Wertes

- addieren des Wertes

- subtrahieren des Wertes

- Multiplizieren des Wertes

 

Im Health Beispiel wird das Setzen und Addieren gezeigt.

 

Das wichtigste ist aber das Hinzufügen und Entfernen der Modifikationen selbst, denn darauf basiert das ganze System und ermöglicht es schnell und einfach beliebige Änderungen vorzunehmen.

Auch CharakterStats Wachstum beim Levelup ist damit ideal umsetzbar wenn man denn noch die Möglichkeit haben möchte den Charakter zu respeccen.

Sprich wenn ich ein LevelDown vornehmen möchte oder gar alle Werte zurücksetzen möchte ist das sehr einfach machbar:

 

var levelUpStatModifications = stat.Modifications.Where(stat => stat.ID == "Levelup").ToArray();
foreach (var modification in levelUpStatModifications)
{
modification.Detach();
availableSkillPoints++;
}

 

Ich hoffe ihr findet es nützlich und könnt damit irgendetwas sinnvolles anfangen.

 

Das ganze besteht aus einigen C# Files, aber davon muss man sich nicht abschrecken lassen um zu wissen was möglich ist reicht ein Blick auf die Interfaces. Im Anhang findet ihr das ganze als zip.

Stats.zip

 

Zuerst der Accumulator, welcher benötigt wird um die Verschiedenen Modifikationen miteinander zu verrechnen:

 

IAccumulator.cs

namespace RPG.Stats
{
public interface IAccumulator<T>
{
	T Accumulate(T left, T right);
}
}

 

Add.cs

namespace RPG.Stats.Accumulators
{
public class Add : IAccumulator<float>
{
	public float Accumulate(float left, float right)
	{
		return left + right;
	}
}
}

 

Multiply.cs

namespace RPG.Stats.Accumulators
{
public class Multiply : IAccumulator<float>
{
	public float Accumulate(float left, float right)
	{
		return left * right;
	}
}
}

 

Set.cs

namespace RPG.Stats.Accumulators
{
public class Set<T> : IAccumulator<T>
{
	public T Accumulate(T left, T right)
	{
		return right;
	}
}
}

 

Subtract.cs

namespace RPG.Stats.Accumulators
{
public class Subtract : IAccumulator<float>
{
	public float Accumulate(float left, float right)
	{
		return left - right;
	}
}
}

 

Dann die Stats selbst:

 

IStat.cs (enthält auch gleich die eventHandler und EventArgumente für die Stat Veränderungs Events)

using System;
using System.Collections.Generic;

namespace RPG.Stats
{
public class StatModifiedEventArgs : EventArgs
{
	public enum ModifyType
	{
		ModificationAdded,
		ModificationRemoved,
		ModificationChanged
	}

	public readonly ModifyType Type;
	public readonly IStatModification Modification;

	public StatModifiedEventArgs(ModifyType type, IStatModification modification)
	{
		Type = type;
		Modification = modification;
	}
}

public class StatModifiedEventArgs<T> : StatModifiedEventArgs
{
	public readonly T OldValue;
	public readonly T NewValue;

	public StatModifiedEventArgs(ModifyType type, IStatModification modification, T oldValue, T newValue)
		: base(type, modification)
	{
		this.OldValue = oldValue;
		this.NewValue = newValue;
	}
}

public delegate void StatModifiedEventHandler(object sender, StatModifiedEventArgs e);

public interface IStat
{
	string Id { get; }
	event StatModifiedEventHandler Modified;
	IEnumerable<IStatModification> Modifications { get; }
}

public interface IStat<T> : IStat
{
	T Value { get; }
}
}

 

Stat.cs

using System.Collections.Generic;

namespace RPG.Stats
{
public class Stat<T> : IStat<T>
{
	private List<IStatModification<T>> modifications = new List<IStatModification<T>>();
	private bool isDirty = true;
	private T value;

	public Stat(string id, IStatModification baseModification = null)
	{
		this.Id = id;
		if (baseModification != null)
			baseModification.Attach(this);
	}

	public Stat(string id, T baseValue)
		: this(id, new StatModification<T>(string.Empty, int.MinValue, new Accumulators.Set<T>(), baseValue))
	{
	}

	public string Id { get; private set; }

	public event StatModifiedEventHandler Modified;

	public IEnumerable<IStatModification> Modifications { get { return modifications; } }

	public T Value
	{
		get
		{
			if (isDirty)
			{
				isDirty = false;
				value = default(T);

				foreach (var modification in modifications)
					value = (modification as StatModification<T>).Accumulate(value);
			}
			return value;
		}
	}

	internal void AddModification(IStatModification<T> modification)
	{
		isDirty = true;

		modifications.Add(modification);
		modifications.Sort((a,  => a.Priority.CompareTo(b.Priority));

		if (Modified != null)
			Modified(this, new StatModifiedEventArgs<T>(StatModifiedEventArgs.ModifyType.ModificationAdded, modification, value, Value));
	}

	internal void RemoveModification(IStatModification<T> modification)
	{
		isDirty = true;

		modifications.Remove(modification);

		if (Modified != null)
			Modified(this, new StatModifiedEventArgs<T>(StatModifiedEventArgs.ModifyType.ModificationRemoved, modification, value, Value));
	}

	internal void FireModificationChanged(IStatModification<T> modification)
	{
		isDirty = true;

		if (Modified != null)
			Modified(this, new StatModifiedEventArgs<T>(StatModifiedEventArgs.ModifyType.ModificationChanged, modification, value, Value));
	}
}
}

 

Und zu guter letzt die Modifikationen:

 

IStatModification.cs

using System;

namespace RPG.Stats
{
public class ModificationModifiedEventArgs : EventArgs
{
	public enum ModifyType
	{
		Added,
		Removed,
		Changed
	}

	public readonly ModifyType Type;
	public readonly IStat Owner;

	public ModificationModifiedEventArgs(ModifyType type, IStat owner)
	{
		Type = type;
		Owner = owner;
	}
}

public class ModificationModifiedEventArgs<T> : ModificationModifiedEventArgs
{
	public readonly T OldValue;
	public readonly T NewValue;

	public ModificationModifiedEventArgs(ModifyType type, IStat owner, T oldValue, T newValue)
		: base(type, owner)
	{
		this.OldValue = oldValue;
		this.NewValue = newValue;
	}
}


public delegate void ModificationModifiedEventHandler(object sender, ModificationModifiedEventArgs e);

public interface IStatModification
{
	string Id { get; }
	int Priority { get; }

	event ModificationModifiedEventHandler Modified;

	IStat Assignee { get; }

	void Attach(IStat stat);
	void Detach();
}

public interface IStatModification<T> : IStatModification
{
	T Value { get; set; }
}
}

 

StatModification.cs

namespace RPG.Stats
{
public class StatModification<T> : IStatModification<T>
{
	private T value;
	private IAccumulator<T> accumulator;

	public StatModification(string id, int priority, IAccumulator<T> accumulator, T value)
	{
		this.Id = id;
		this.Priority = priority;
		this.accumulator = accumulator;
		this.Value = value;
	}

	public string Id { get; private set; }

	public int Priority { get; private set; }

	public event ModificationModifiedEventHandler Modified;

	public IStat Assignee { get; private set; }

	public T Value
	{
		get
		{
			return value;
		}
		set
		{
			if (object.Equals(this.value, value))
				return;
			var oldValue = this.value;
			this.value = value;
			if (Assignee != null)
				(Assignee as Stat<T>).FireModificationChanged(this);
			if (Modified != null)
				Modified(this, new ModificationModifiedEventArgs<T>(ModificationModifiedEventArgs.ModifyType.Changed, Assignee, oldValue, value));
		}
	}

	public void Attach(IStat stat)
	{
		if (Assignee != null)
			return;

		Assignee = stat;

		(Assignee as Stat<T>).AddModification(this);
		if (Modified != null)
			Modified(this, new ModificationModifiedEventArgs<T>(ModificationModifiedEventArgs.ModifyType.Added, stat, value, value));
	}

	public void Detach()
	{
		if (Assignee == null)
			return;

		var oldAssignee = Assignee;
		Assignee = null;
		(oldAssignee as Stat<T>).RemoveModification(this);
		if (Modified != null)
			Modified(this, new ModificationModifiedEventArgs<T>(ModificationModifiedEventArgs.ModifyType.Removed, oldAssignee, value, value));
	}

	internal T Accumulate(T currentValue)
	{
		return accumulator.Accumulate(currentValue, value);
	}
}
}

Link zu diesem Kommentar
Auf anderen Seiten teilen

Es wird Zeit, dass das Syntax Highlighting wieder Einzug ins Forum hält...

 

Interessanter Ansatz! Werde ich mir nochmal genau durchlesen. Ohne Syntax Highlighting... ist das nicht so cool ;)

 

Da ich an ähnlichem werkel, ist das Thema sehr spanned für mich. Trotzdem habe ich mich für ein eher starreres System entschieden und nutze enums. Auch dort gibt es nur ein paar Stellen, an denen man Hand anlegen muss, wenn man es in einem anderen Projekt mit anderen Stats nutzen will.

Link zu diesem Kommentar
Auf anderen Seiten teilen

ACM: Mit Enums meinst du EnumValues anstatt Namen?

 

Ich glaube irgendwo hier im Forum hatten wir das schonmal in der Art und ich finde Enums in dem Fall auch viel besser. Musste aber strings für die Erweiterbarkeit nehmen. Das Problem kann ich aber weitgehendst umgehen indem ich beides kombiniere:

 

enum StatNames
{
 Health,
 Strength,
 Dextery
 NutSize
 Mana
}

...

var healthStat = new Stat<float>(StatNames.Health.ToString(), 100);

 

Auch möglich wäre dass ich den Stat CTor Parameter für die ID von string auf object ändere und dann das hier mache:

 

public Stat(object id, T baseValue)
{
 this.ID = id.ToString();
}

 

Aber das würde dann wieder verdecken dass die ID die reingegeben wird zu einen String wird.

 

Oder ich arbeite einfach mit const strings, wäre auch machbar.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Man kann den Code so anpassen dass anstatt einer string ID einfach ein enum genommen wird, oder noch cooler die ID per Generic typisieren. Hab auch wirklich mit dem Gedanken gespielt das so zu machen, aber war mir dann doch etwas zuviel des Guten ;)

 

Besonders da es unter Umständen echt viele Stats gibt (man kann dadurch auch Skillsets darstellen, Level, Skillpoints, etc)

 

Ausserdem sollte es ein ähnliches System auch für die Modifikationen selbst geben und da kann es auch wieder hunderte geben. Die Möglichkeit die IDs der Modifikationen und Stats mit einer DB für Beschreibungen etc zu kombinieren hat mich dann doch sehr zu diesem string basierten IDs getrieben.

Link zu diesem Kommentar
Auf anderen Seiten teilen

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Gast
Auf dieses Thema antworten...

×   Du hast formatierten Text eingefügt.   Formatierung jetzt entfernen

  Only 75 emoji are allowed.

×   Dein Link wurde automatisch eingebettet.   Einbetten rückgängig machen und als Link darstellen

×   Dein vorheriger Inhalt wurde wiederhergestellt.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Lädt...
×
×
  • Neu erstellen...