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);
}
}
}