Problem with serialization of object in unity - c#

This is the class I want to serialize:
System.Serializable]
public class Save
{
public int turn, player;
public int build, attack, action;
public double resource, resourceProd;
public List<ObjectData> objectsData = new List<ObjectData>();
public Save(GameObject[] saveBuilding, GameObject[] saveUnit, int turn, int player, int build, int attack, int action, double resource, double resourceProd)
{
foreach(GameObject building in saveBuilding)
{
ObjectData data = new ObjectData();
data.position = building.transform.position;
data.building = building.GetComponent<Building>();
data.path = data.building.type[0] + data.building.level + data.building.branch;
objectsData.Add(data);
}
foreach(GameObject unit in saveUnit)
{
ObjectData data = new ObjectData();
data.position = unit.transform.position;
data.unit = unit.GetComponent<Unit>();
data.path = data.unit.type[0] + data.unit.level + data.unit.branch;
objectsData.Add(data);
}
this.turn = turn;
this.player = player;
this.build = build;
this.attack = attack;
this.action = action;
this.resource = resource;
this.resourceProd = resourceProd;
Debug.Log(objectsData.Count);
}
}
public class ObjectData
{
public Vector3 position;
public Building building;
public Unit unit;
public string path;
}
When I creat a instance of it with the method below, there are two element in objectsData according toDebug.Log(objectsData.Count);.
public static void SaveTurn()
{
turnSave = new Save(GameObject.FindGameObjectsWithTag("building"), GameObject.FindGameObjectsWithTag("unit"), turnCourt, Players.player, BuildDisplay.maxBuildAction, Attacking.maxAttackAction, Building.actions, Currency.resource, Building.totalprod);
solution.Add(JsonUtility.ToJson(turnSave));
Debug.Log(JsonUtility.ToJson(turnSave));
}
When I try the load the save Debug.Log(save.objectsData); returns null.
Why does this happens?
public static void LoadSave(string saves)
{
Save save = JsonUtility.FromJson<Save>(saves);
Currency.resource = save.resource;
Building.totalprod = save.resourceProd;
BuildDisplay.maxBuildAction = save.build;
Attacking.maxAttackAction = save.attack;
Building.actions = save.action;
Debug.Log(save.objectsData);
LoadObjects(save.objectsData);
RemoveObjects(GameObject.FindGameObjectsWithTag("building"));
RemoveObjects(GameObject.FindGameObjectsWithTag("unit"));
Turn.refresh();
}
public static void LoadObjects(List<ObjectData> gameObjects)
{
Debug.Log(gameObjects.Count);
foreach(ObjectData gameObject in gameObjects)
{
GameObject Prefab = Resources.Load(gameObject.path) as GameObject;
if(gameObject.building != null)
{
Building building = Prefab.GetComponent<Building>();
building = gameObject.building;
}
if(gameObject.unit != null)
{
Unit unit = Prefab.GetComponent<Unit>();
unit = gameObject.unit;
}
Instantiate(Prefab);
}
}
After making all class involved serializable Debug.Log(save.objectsData); no longer returns null and Debug.Log(gameObjects.Count); returns 2, but there is a null exception at Building building = Prefab.GetComponent<Building>(); even though there is a Building script on the Prefab.

First of all all classes you want to serialize need to have the attribute [Serializable] so also the ObjetData
[Serializable]
public class ObjectData
{
...
}
Then another "issue" here is that Unit and Building are both MonoBehaviour which derives from UnityEngine.Object. All types deriving from UnityEngine.Object are always only (de)serialized as instance references, never actually containing there according field values.
There are two similar ways around this.
Option A
Instead of serializing the Unit and Building directly you could rather serialize some dedicated data class that basically reflects the components field values but in a serializable container.
E.g. let's say your classes look like
public class Unit : MonoBehaviour
{
[SerializeField] private string _name;
[SerializeField] private int _id;
...
}
Then you could create a container like
[Serializable]
public class UnitData
{
public string Name;
public int ID;
}
And then in order to maintain capsulation and keep your fields private I would add according methods to your types themselves like e.g.
public class Unit : MonoBehaviour
{
[SerializeField] private string _name;
[SerializeField] private int _id;
public UnitContainer GetData()
{
return new UnitContainer()
{
Name = _name,
ID = _id;
}
}
public void SetData(UnitData data)
{
_name = data.Name;
_id = data.ID;
// ... Probably react to changed data
}
}
This way you could also store values that are usually not serialized like properties or private fields.
Same for the Building.
Then you could rather store these in your data like
[Serializable]
public class ObjectData
{
public Vector3 position;
public BuildingData building;
public UnitData unit;
public string path;
}
Then in your Save class you would make sure to create these data classes accordingly
foreach(var building in saveBuilding)
{
var data = new ObjectData()
{
position = building.transform.position,
building = building.GetComponent<Building>().GetData(),
path = data.building.type[0] + data.building.level + data.building.branch
};
objectsData.Add(data);
}
foreach(var unit in saveUnit)
{
var data = new ObjectData()
{
position = unit.transform.position,
unit = unit.GetComponent<Unit>().GetData(),
path = data.unit.type[0] + data.unit.level + data.unit.branch
};
objectsData.Add(data);
}
And for loading accordingly
foreach(var data in objectDatas)
{
var prefab = (GameObject) Resources.Load(data.path);
if(data.building != null)
{
var building = Instantiate (prefab).GetComponent<Building>();
building.SetData(data.building);
}
else if(data.unit != null)
{
var unit = Instantiate(prefab).GetComponent<Unit>();
unit.SetData(data.unit);
}
}
Option B
Actually quite similar but without the need for an extra data container class you could rather again (de)serialize your components directly using JSON like e.g.
public class Unit : MonoBehaviour
{
...
public string GetData()
{
return JsonUtility.ToJson(this);
}
public void SetData(string json)
{
JsonUtility.FromJsonOverwrite(json, this);
// ... React to changed data
}
}
This automatically includes all fields that can be serialized (are either public or tagged [SerializeField] and have a serializable type).
And then rather store these json strings like
[Serializable]
public class ObjectData
{
public Vector3 position;
public string building;
public string unit;
public string path;
}
The methods for save and load would be the same as for Option A above.

Related

Serializing / De-serializing inherited class on a procedural generated loot system (Unity C#)

I have a class called Item that has general info about the item (Name,ID, description)
I inherit from this class to create procedurally Armor / Weapons in my game.
Eg:
[System.Serializable]
public class Item
{
public string itemName;
public string itemID;
public string itemDescription;
}
[System.Serializable]
public class Armor : Item
{
public int defense;
public int armorValue;
}
In the game, when you kill an enemy, armor class procedurally generates stats and values.
My inventory system has a list of slots, that use Item class as a reference to what item is in the slot.
When I save with Json the slots, and then load again, inheritance does not work, so I get an Item class, with all the base class variables that the Armor class that was "saved" had. I have tried:
JsonConver.Serialize, JsonConvert.Deserialize explicitely on each slot (didnt work)
Casting Item as Armor if item is armor, that didnt work
Using the [JsonConstructor] attribute on the Armor constructor
I am new to Json, Serialization / De-Serialization and I try to read and understand as much as I can so any ideas / leads would be great. Thanks!
EDIT: I solved it by using Dictionaries<string,Armor> storing specifically any armors picked up using Item.ID, so on Equip, takes the armor from the dictionary, no need for casting.
#YungDeiza
[System.Serializable]
public class InventorySlot
{
public Item itemInSlot //Item is the base class
//First the slot is initialized with the normal JsonUtilities.FromJson, from my SaveLoad class. That works.
//I now have a reference to the Item so also Access to the item.ID
//This is how I tested JsonConvert.Serialize/Desirialize
public void SaveSlot()
{
if(item is Armor)
{
var json = JsonConvert.Serialize(item as Armor);
PlayerPrefs.SetString(item.ID, json);
}
}
public void LoadSlot()
{
if(PlayerPrefs.HasKey(item.ID))
{
var json = PlayerPrefs.GetString(item.ID);
var armor = JsonConvert.DeSerialize<Armor>(json);
item = new Armor(armor); //I have made a constructor for the Armor class that takes an Armor and "copies" it
}
}
}
That is my Save system:
public static class SaveLoad
{
public static UnityAction OnSaveGame;
public static UnityAction<SaveData> OnLoadGame;
public const string SaveDirectory = "/SaveData/";
private const string FileName = "Save.sav";
public static bool Save(SaveData saveData)
{
OnSaveGame?.Invoke();
string dir = Application.persistentDataPath + SaveDirectory;
GUIUtility.systemCopyBuffer = dir;
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
string json = JsonUtility.ToJson(saveData, true);
File.WriteAllText(dir + FileName, json);
return true;
}
public static void DeleteSavedData()
{
string fullPath = Application.persistentDataPath + SaveDirectory + FileName;
if (File.Exists(fullPath)) File.Delete(fullPath);
}
public static SaveData Load()
{
string fullPath = Application.persistentDataPath + SaveDirectory + FileName;
SaveData data = new SaveData();
if (File.Exists(fullPath))
{
string json = File.ReadAllText(fullPath);
data = JsonUtility.FromJson<SaveData>(json);
OnLoadGame?.Invoke(data);
}
else
{
Debug.LogWarning("Save File Does not exist");
}
return data;
}
}
And hereby what is the data is saved:
[System.Serializable]
public void PlayerData
{
//Banch of stats - intergers
public InventorySystem inventorySystem;
public PlayerData()
{
//Stats are initialized
inventorySystem = new InventorySystem(x); //I have a default inventory size of 24
}
}
[System.Serializable]
public void InventorySystem
{
public List<InventorySlots> inventorySlots;
public void InventorySystem(int size)
{
inventorySlots = new List<InventorySlots>(size);
}
}
As I said, when an enemy is killed, he drops an armor, and the armor class itself, just calculates random stats. The saveload method normally saves the Armor, and I can see it in my inventory, but I used a Debug.Log(item.GetType), and it always loads it as an Item class and not an Armor class. I ve read a lot, and as far as I can understand, Unity serialization and JsonUtilities, dont really support inheritance. So I m not sure where to look now.
Thank you in advance, I hope I helped understand my issue a bit better

Generate a strongly-typed proxy that can track changes on property names not values when one property is set to another

Setup:
public class Data
{
public int A { get; set; }
public int B { get; set; }
}
public class Runner
{
public static void Run(Data data)
{
data.A = data.B;
data.A = 1;
}
}
class Program
{
static void Main(string[] args)
{
var data = new Data() { A = 1, B = 2 };
Runner.Run(data);
}
}
Problem: I need to implement change tracking here for property names not values. Inside Runner.Run on the first line data.A = data.B I need to record somehow that "A" was set to "B" (literally property names) and then on the next line data.A = 1 I need to record that "A" was set to constant and say forget about it.
Constrains:
When setting one property to another (e.g. A = B) that needs to be recorded
When setting property to anything else (e.g. A = 1 or A = B * 2) this change needs to be forgotten (e.g. remember A only)
Suppose this is the tracker contract being used:
void RecordChange(string setterName, string getterName);
void UnTrackChange(string setterName);
Question:
I would like to somehow proxy the Data class so it still can be used in the interface code (e.g. Runner - is a whole bunch of a business logic code that uses Data) INCLUDING strong-typing and it can track it's changes without modifying the code (e.g. there is lots of places like 'data.A = data.B').
Is there any way to do it without resorting to I guess some magic involving IL generation?
Already investigated/tried:
PostSharp interceptors/Castle.DynamicProxy with interceptors - these alone cannot help. The most I can get out of it is to have a value of data.B inside setter interceptor but not nameof(data.B).
Compiler services - haven't found anything suitable here - getting the name of caller doesn't really help.
Runtine code generation - smth like proxy inherited from DynamicObject or using Relfection.Emit (TypeBuilder probably) - I lose typings.
Current solution:
Use the Tracker implementation of the abovementioned contract and pass it around into every function down the road. Then instead of writing data.A = data.B use method tracker.SetFrom(x => x.A, x => x.B) - tracker holds a Data instance and so this works. BUT in a real codebase it is easy to miss something and it just makes it way less readable.
It is the closest the solution I've come up with. It isn't perfect as I still need to modify all the contracts/methods in the client code to use a new data model but at least all the logic stays the same.
So I'm open for other answers.
Here's the renewed Data model:
public readonly struct NamedProperty<TValue>
{
public NamedProperty(string name, TValue value)
{
Name = name;
Value = value;
}
public string Name { get; }
public TValue Value { get; }
public static implicit operator TValue (NamedProperty<TValue> obj)
=> obj.Value;
public static implicit operator NamedProperty<TValue>(TValue value)
=> new NamedProperty<TValue>(null, value);
}
public interface ISelfTracker<T>
where T : class, ISelfTracker<T>
{
Tracker<T> Tracker { get; set; }
}
public class NamedData : ISelfTracker<NamedData>
{
public virtual NamedProperty<int> A { get; set; }
public virtual NamedProperty<int> B { get; set; }
public Tracker<NamedData> Tracker { get; set; }
}
Basically I've copy-pasted the original Data model but changed all its properties to be aware of their names.
Then the tracker itself:
public class Tracker<T>
where T : class, ISelfTracker<T>
{
public T Instance { get; }
public T Proxy { get; }
public Tracker(T instance)
{
Instance = instance;
Proxy = new ProxyGenerator().CreateClassProxyWithTarget<T>(Instance, new TrackingNamedProxyInterceptor<T>(this));
Proxy.Tracker = this;
}
public void RecordChange(string setterName, string getterName)
{
}
public void UnTrackChange(string setterName)
{
}
}
The interceptor for Castle.DynamicProxy:
public class TrackingNamedProxyInterceptor<T> : IInterceptor
where T : class, ISelfTracker<T>
{
private const string SetterPrefix = "set_";
private const string GetterPrefix = "get_";
private readonly Tracker<T> _tracker;
public TrackingNamedProxyInterceptor(Tracker<T> proxy)
{
_tracker = proxy;
}
public void Intercept(IInvocation invocation)
{
if (IsSetMethod(invocation.Method))
{
string propertyName = GetPropertyName(invocation.Method);
dynamic value = invocation.Arguments[0];
var propertyType = value.GetType();
if (IsOfGenericType(propertyType, typeof(NamedProperty<>)))
{
if (value.Name == null)
{
_tracker.UnTrackChange(propertyName);
}
else
{
_tracker.RecordChange(propertyName, value.Name);
}
var args = new[] { propertyName, value.Value };
invocation.Arguments[0] = Activator.CreateInstance(propertyType, args);
}
}
invocation.Proceed();
}
private string GetPropertyName(MethodInfo method)
=> method.Name.Replace(SetterPrefix, string.Empty).Replace(GetterPrefix, string.Empty);
private bool IsSetMethod(MethodInfo method)
=> method.IsSpecialName && method.Name.StartsWith(SetterPrefix);
private bool IsOfGenericType(Type type, Type openGenericType)
=> type.IsGenericType && type.GetGenericTypeDefinition() == openGenericType;
}
And the modified entry point:
static void Main(string[] args)
{
var data = new Data() { A = 1, B = 2 };
NamedData namedData = Map(data);
var proxy = new Tracker<NamedData>(namedData).Proxy;
Runner.Run(proxy);
Console.ReadLine();
}
The Map() function actually maps Data to NamedData filling in property names.

Iterate a dictionary of interfaces as specific type

I have a dictionary of string/interface class. I cannot include the fields I need in the interface child-classes, but each of them have the same public variables I need to change.
I want to loop through the dictionary and change them to values within the looping class. I cannot do so, as the interface does not contain those variables.
How should I go about this?
public class MakeAbility : MonoBehaviour
{
public BlockScriptableObject block;
public IDictionary<string, IAbility> abilities_parts = new Dictionary<string, IAbility>();
public Targeting target_manager;
public AbAttack attack;
public AbCast cast;
public AbDefend defend;
public AbDefendOther defend_other;
public AbPotion potion;
private void Start()
{
abilities_parts.Add("attack", attack);
abilities_parts.Add("cast", cast);
abilities_parts.Add("defend", defend);
abilities_parts.Add("defend_other", defend_other);
abilities_parts.Add("potion", potion);
}
public void trigger_button()
{
foreach (var i in abilities_parts.Values)
{
i.block_attack_damage = block.attack_damage;
i.targeting_for_attack = target_manager;
}
public interface IAbility
{
void Use();
void Enact();
}
public class AbPotion : MonoBehaviour, IAbility
{
public Targeting targeting_for_attack;
public int block_attack_damage = 10;
public void Use()
{
}
public void Enact()
{
}
}
Your properties are NOT properties of IAbility. They are properties of the AbPotion class. You will need an if else statement on the types to set them individually. The thing is, they should have been set before adding to the Dictionary and probably thru the constructor.
public void trigger_button()
{
foreach (var i in abilities_parts.Values)
{
if(i is AbPotion)
{
var potion = i as AbPotion;
potion.block_attack_damage = block.attack_damage;
potion.targeting_for_attack = target_manager;
}
else if(i is AbAttack)
{
var attack = i as AbAttack;
attack.Property1= value1;
attack.Property2 = value2;
}
}
}

How to automate unit testing of object state?

I have this serializable class which is my class for persisting game data.
[Serializable]
class GameData
{
public float experience = Helper.DEFAULT_EXPERIENCE;
public float score = Helper.DEFAULT_SCORE;
public float winPercent = Helper.DEFAULT_WIN_PERCENT;
public int tasksSolved = Helper.DEFAULT_NUM_OF_TASKS_SOLVED;
public int correct = Helper.DEFAULT_NUM_OF_CORRECT;
public int additions = Helper.DEFAULT_NUM_OF_ADDITIONS;
public int subtractions = Helper.DEFAULT_NUM_OF_SUBTRACTIONS;
public bool useAddition = Helper.DEFAULT_USE_ADDITION;
public bool useSubtraction = Helper.DEFAULT_USE_SUBTRACTION;
public bool useIncrementalRange = Helper.DEFAULT_USE_INCREMENTAL_RANGE;
public bool gameStateDirty = Helper.DEFAULT_GAME_STATE_DIRTY;
public bool gameIsNormal = Helper.DEFAULT_GAME_IS_NORMAL;
public bool operandsSign = Helper.DEFAULT_OPERANDS_SIGN;
}
The class that utilizes this serializable class looks like this:
using UnityEngine;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class SaveLoadGameData : MonoBehaviour
{
public static SaveLoadGameData gameState;
public float experience = Helper.DEFAULT_EXPERIENCE;
public float score = Helper.DEFAULT_SCORE;
public float winPercent = Helper.DEFAULT_WIN_PERCENT;
public int tasksSolved = Helper.DEFAULT_NUM_OF_TASKS_SOLVED;
public int correct = Helper.DEFAULT_NUM_OF_CORRECT;
public int additions = Helper.DEFAULT_NUM_OF_ADDITIONS;
public int subtractions = Helper.DEFAULT_NUM_OF_SUBTRACTIONS;
public bool useAddition = Helper.DEFAULT_USE_ADDITION;
public bool useSubtraction = Helper.DEFAULT_USE_SUBTRACTION;
public bool useIncrementalRange = Helper.DEFAULT_USE_INCREMENTAL_RANGE;
public bool gameStateDirty = Helper.DEFAULT_GAME_STATE_DIRTY;
public bool gameIsNormal = Helper.DEFAULT_GAME_IS_NORMAL;
public bool operandsSign = Helper.DEFAULT_OPERANDS_SIGN;
void Awake () {}
public void init ()
{
if (gameState == null)
{
DontDestroyOnLoad(gameObject);
gameState = this;
}
else if (gameState != this)
{
Destroy(gameObject);
}
}
public void SaveForWeb ()
{
UpdateGameState();
try
{
PlayerPrefs.SetFloat(Helper.EXP_KEY, experience);
PlayerPrefs.SetFloat(Helper.SCORE_KEY, score);
PlayerPrefs.SetFloat(Helper.WIN_PERCENT_KEY, winPercent);
PlayerPrefs.SetInt(Helper.TASKS_SOLVED_KEY, tasksSolved);
PlayerPrefs.SetInt(Helper.CORRECT_ANSWERS_KEY, correct);
PlayerPrefs.SetInt(Helper.ADDITIONS_KEY, additions);
PlayerPrefs.SetInt(Helper.SUBTRACTIONS_KEY, subtractions);
PlayerPrefs.SetInt(Helper.USE_ADDITION, Helper.BoolToInt(useAddition));
PlayerPrefs.SetInt(Helper.USE_SUBTRACTION, Helper.BoolToInt(useSubtraction));
PlayerPrefs.SetInt(Helper.USE_INCREMENTAL_RANGE, Helper.BoolToInt(useIncrementalRange));
PlayerPrefs.SetInt(Helper.GAME_STATE_DIRTY, Helper.BoolToInt(gameStateDirty));
PlayerPrefs.SetInt(Helper.OPERANDS_SIGN, Helper.BoolToInt(operandsSign));
PlayerPrefs.Save();
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
}
public void SaveForX86 () {}
public void Load () {}
public void UpdateGameState () {}
public void ResetGameState () {}
}
Note: GameData is inside the same file with SaveLoadGameData class.
As you can see GameData class has ton of stuff and creating test for each function inside SaveLoadGameData class is long and boring process. I have to create a fake object for each property inside GameData and test the functionality of the functions in SaveLoadGameData do they do what they are supposed to do.
Note: This is Unity3D 5 code and testing MonoBehaviors with stubs and mocks is almost immposible. Therefore I created helper function that creates fake object:
SaveLoadGameData saveLoadObject;
GameObject gameStateObject;
SaveLoadGameData CreateFakeSaveLoadObject ()
{
gameStateObject = new GameObject();
saveLoadObject = gameStateObject.AddComponent<SaveLoadGameData>();
saveLoadObject.init();
saveLoadObject.experience = Arg.Is<float>(x => x > 0);
saveLoadObject.score = Arg.Is<float>(x => x > 0);
saveLoadObject.winPercent = 75;
saveLoadObject.tasksSolved = 40;
saveLoadObject.correct = 30;
saveLoadObject.additions = 10;
saveLoadObject.subtractions = 10;
saveLoadObject.useAddition = false;
saveLoadObject.useSubtraction = false;
saveLoadObject.useIncrementalRange = true;
saveLoadObject.gameStateDirty = true;
saveLoadObject.gameIsNormal = false;
saveLoadObject.operandsSign = true;
return saveLoadObject;
}
How would you automate this process?
Yes two asserts inside one test is a bad practice but what would you do instead?
Example test for SaveForWeb()
[Test]
public void SaveForWebTest_CreateFakeGameStateObjectRunTheFunctionAndCheckIfLongestChainKeyExists_PassesIfLongestChainKeyExistsInPlayerPrefs()
{
// arrange
saveLoadObject = CreateFakeSaveLoadObject();
// act
saveLoadObject.SaveForWeb();
// assert
Assert.True(PlayerPrefs.HasKey(Helper.LONGEST_CHAIN_KEY));
Assert.AreEqual(saveLoadObject.longestChain, PlayerPrefs.GetInt(Helper.LONGEST_CHAIN_KEY, Helper.DEFAULT_LONGEST_CHAIN));
GameObject.DestroyImmediate(gameStateObject);
}
Since Helper is static class containing only public constants I had to use BindingFlags.Static and BindingFlags.Public to iterate over its members, so I used this code snippet to automate asserting over several fields of different type:
FieldInfo[] helperFields = typeof(SaveLoadGameData).GetFields();
FieldInfo[] defaults = typeof(Helper).GetFields(BindingFlags.Static | BindingFlags.Public);
for(int i = 0; i < defaults.Length; i += 1)
{
Debug.Log(helperFields[i].Name + ", " + helperFields[i].GetValue(saveLoadObject) + ", " + defaults[i].GetValue(null));
Assert.AreEqual(helperFields[i].GetValue(saveLoadObject), defaults[i].GetValue(null));
}
Note: defaults and helperFields have the same length as I am checking if helperFields have the default values after using ResetGameState().
Though this answer is about ResetGameState() instead of SaveForWeb() function, this code can be applied wherever possible.

Static Instances and Enums for referencing common properties

I'm working on a problem at the moment and I've run into an issue where I have multiple architectural options, but am not sure which is going to be the best option going forward.
Context:
I am writing some code for a game, which uses a tile map. Tiles have common attributes, for example, all floor tiles are walkable, while walls are not (along with other properties). Therefore, it makes sense to have some kind of reference to which each tile can point at a common reference to discern what its properties are.
I have come up with a few solutions, however am uncertain as to which is the most efficient or will provide the greatest flexibility moving forward. Therefore, I am curious as to which would be considered 'best', either in general or for my specific case. Similarly, if there is a better way of doing this that I haven't listed, please let me know.
(As an aside, as the number of tile types grow, I may also run into the issue where it may not be practical to hard code these values and some kind of serialization or file I/O might make more sense. As I have done neither in C#, if you see any potential stumbling blocks here, it would be similarly appreciated if you could include them in your answer.)
Below are each of my three approaches, which I have simplified slightly to make them more general:
Approach #1: Enum with Extension Methods:
public enum TileData{
WALL,
FLOOR,
FARMLAND
//...etc
}
public static class TileDataExtensions{
public static int IsWalkable(this TileData tile){
switch(tile){
case TileData.FLOOR:
case TileData.FARMLAND:
return true;
case TileData.WALL:
return false;
}
}
public static int IsBuildable(this TileData tile){
switch(tile){
case TileData.FLOOR:
return true;
case TileData.WALL:
case TileData.FARMLAND:
return false;
}
}
public static Zone ZoneType(this TileData tile){
switch(tile){
case TileData.WALL:
case TileData.FLOOR:
return Zone.None;
case TileData.FARMLAND:
return Zone.Arable;
}
}
public static int TileGraphicIndex(this TileData tile){
switch(tile){
case TileData.WALL:
return 0;
case TileData.FLOOR:
return 1;
case TileData.FARMLAND:
return 2;
}
}
public enum Zone{
Shipping,
Receiving,
Arable,
None
}
}
Approach #2: Huge Private Constructor & Static Instances
public class TileData{
public bool IsWalkable{get;};
public bool IsBuildSpace{get;};
public Zone ZoneType{get;};
public int TileGraphicIndex{get;};
public static TileData FLOOR = new TileData(true, true, Zone.None, 1);
public static TileData WALL = new TileData(false, false, Zone.None, 0);
public static TileData FARMLAND = new TileData(true, false, Zone.Arable, 2);
//...etc
private TileData(bool walkable, bool buildSpace, Zone zone, int grahpicIndex){
IsWalkable = walkable;
IsBuildSpace = buildSpace;
ZoneType = zone;
TileGraphicIndex = grahpicIndex;
}
public enum Zone{
Shipping,
Receiving,
Arable,
None
}
}
Approach #3: Private Constructor and Setters, with Static Instances:
public class TileData{
public bool IsWalkable{get; private set;};
public bool IsBuildSpace{get; private set;};
public Zone ZoneType{get; private set;};
public int TileGraphicIndex{get; private set;};
public static TileData FLOOR{
get{
TileData t = new TileData();
t.IsBuildSpace = true;
t.TileGraphicIndex = 1;
return t;
}
}
public static TileData WALL{
get{
TileData t = new TileData();
t.IsWalkable = false;
return t;
}
}
public static TileData FARMLAND{
get{
TileData t = new TileData();
t.ZoneType = Zone.Arable;
t.TileGraphicIndex = 2;
return t;
}
}
//...etc
//Constructor applies the most common values
private TileData(){
IsWalkable = true;
IsBuildSpace = false;
ZoneType = Zone.None;
TileGraphicIndex = 0;
}
public enum Zone{
Shipping,
Receiving,
Arable,
None
}
}
Many thanks, LR92
EDIT: The types of tiles are determined before compile time by the designer, ie no class should be allowed to create new TileData types (ie, in examples 2&3, instances).
Approach 2 is friendly to the designer and is slightly more efficient than Approach 3. It can also be supplemented by Approach 1's extension methods if you want to do some reasoning system-by-system instead of tile-by-tile.
Consider supplementing your constructor with a static factory:
private TileData(bool walkable, bool buildSpace, Zone zone, int grahpicIndex){
IsWalkable = walkable;
IsBuildSpace = buildSpace;
ZoneType = zone;
TileGraphicIndex = grahpicIndex;
}
private static TileData Tweak(TileData parent, Action<TileData> tweaks) {
var newTile = parent.MemberwiseClone();
tweaks(newTile);
return newTile;
}
This allows you to build your tile types with a sort of prototypal inheritance (except instead of looking up the chain of prototypes at runtime, it will be baked in). This should be very useful as it is common in tile-based games to have tiles that are mostly similar but have slightly different behaviors or graphics.
public readonly static TileData GRASS = new TileData(etc.);
public readonly static TileData WAVY_GRASS = Tweak(GRASS, g => g.TileGraphicIndex = 10);
public readonly static TileData JERKFACE_GRASS = Tweak(GRASS, g => g.IsWalkable = false);
public readonly static TileData SWAMP_GRASS = Tweak(GRASS, g => {g.TileGraphicIndex = 11; g.IsBuildable = false;});
Note: when you serialize/deserialize your tile maps, you'll want to have a consistent ID of some sort assigned to each tile (in particular, this makes working with Tiled easier). You could pass that in to the constructor (and to Tweak, as another argument, because otherwise the tweaked tile will have cloned the ID of its parent!). It would be good then to have something (a unit test would be fine) that ensures that all fields of this class of type TileData have distinct IDs. Finally, to avoid having to re-enter these IDs into Tiled, you could make something that exports the data from this class into a Tiled TSX or TMX file (or similar file for whatever map editor you ultimately go with).
EDIT: One last tip. If your consistent IDs are consecutive ints, you can "compile" your tile data into static arrays split out by property. This can be useful for systems in which performance is important (for example, pathfinding will need to look up walkability a lot).
public static TileData[] ById = typeof(TileData)
.GetFields(BindingFlags.Static | BindingFlags.Public)
.Where(f => f.FieldType == typeof(TileData))
.Select(f => f.GetValue(null))
.Cast<TileData>()
.OrderBy(td => td.Id)
.ToArray();
public static bool[] Walkable = ById.Select(td => td.IsWalkable).ToArray();
// now you can have your map just be an array of array of ids
// and say things like: if(TileData.Walkable[map[y][x]]) {etc.}
If your ids are not consecutive ints, you can use Dictionary<MyIdType, MyPropertyType> for the same purpose and access it with the same syntax, but it wouldn't perform as well.
Let's try to solve your requirement with more object oriented approach. Less conditional more polymorphism. In my opinion if you have more chances to come up with new Types of Tiles apart from the mentioned ones. Means the design should be extensible and should be open for minimal change to introduce new component.
For e.g. Let's keep the Tile class a base class.
public abstract class Tile
{
public Tile()
{
// Default attributes of a Tile
IsWalkable = false;
IsBuildSpace = false;
ZoneType = Zone.None;
GraphicIndex = -1;
}
public virtual bool IsWalkable { get; private set; }
public virtual bool IsBuildSpace { get; private set; }
public virtual Zone ZoneType { get; private set; }
public virtual int GraphicIndex { get; private set; }
/// <summary>
/// Factory to build the derived types objects
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Get<T>() where T : Tile, new()
{
return new T();
}
}
Now we have defined a Tile with default attributes. If required more default attributes of a Tile can be added as Vitual properties. Since this class is abstract one can not simply create the object so a Derived class has to be introduced which would be our specific type of Tile e.g. Wall, Floor etc.
public class Floor : Tile
{
public override bool IsBuildSpace
{
get { return true; }
}
public override bool IsWalkable
{
get { return true; }
}
public override int GraphicIndex
{
get { return 1; }
}
}
public class Wall : Tile
{
public override int GraphicIndex
{
get { return 0; }
}
public override Zone ZoneType
{
get { return Zone.Arable; }
}
}
If a new type of tile has to be created. Just inherit the class from Tile and Override the properties which would require to have specific values instead of defaults.
Crafting a tile would be done via base class just by invoking the generic static factory method Get<>() that will only accept a derived type of Tile:
Tile wallLeft = Tile.Get<Wall>();
Tile floor = Tile.Get<Floor>();
So Everything is Tile and represents a different set of values of defined properties. They can be identified Either by their type or values of properties. And more importantly as you can see we got rid of all the If..Else, Switch case, Constructor overloads. Hows that sound?
Extending the Tile with new attributes
So for e.g. a new property/attribute is required on Tiles e.g. Color simple add a Virtual property to Tile class named Color. In constructor give it a default value. Optinally (not mandatory) Override the property in child classes if your tile should be in special color.
Introducing new Type of Tile
Simply derive the New Tile type with Tile class and override required properties.
Why not just overload the constructor?
public class TileData{
public bool IsWalkable{get;};
public bool IsBuildSpace{get;};
public Zone ZoneType{get;};
public int TileGraphicIndex{get;};
public static TileData FLOOR = new TileData(true, true, Zone.None, 1);
public static TileData WALL = new TileData(false, false, Zone.None, 0);
public static TileData FARMLAND = new TileData(true, false, Zone.Arable, 2);
//...etc
public TileData(bool walkable, bool buildSpace, Zone zone, int grahpicIndex){
IsWalkable = walkable;
IsBuildSpace = buildSpace;
ZoneType = zone;
TileGraphicIndex = grahpicIndex;
}
public TileData(){
IsWalkable = true;
IsBuildSpace = false;
ZoneType = Zone.None;
TileGraphicIndex = 0;
}
public enum Zone{
Shipping,
Receiving,
Arable,
None
}
}
What about methods to create each type of tile.
public class Tile{
public TileType Type { get; private set; }
public bool IsWalkable { get; private set; }
public bool IsBuildSpace { get; private set; }
public Zone ZoneType { get; private set; }
public int TileGraphicIndex { get; private set; }
private Tile() {
}
public static Tile BuildTile(TileType type){
switch (type) {
case TileType.WALL:
return BuildWallTile();
case TileType.FLOOR:
return BuildFloorTile();
case TileType.FARMLAND:
return BuildFarmlandTile();
default:
throw ArgumentException("type");
}
}
public static Tile BuildWallTile()
{
return new Tile {
IsWalkable = false,
IsBuildSpace = false,
ZoneType = Zone.None,
TileGraphicIndex = 1,
Type = TileType.WALL
};
}
public static Tile BuildFloorTile()
{
return new Tile {
IsWalkable = true,
IsBuildSpace = None,
ZoneType = Zone.None,
TileGraphicIndex = 1,
Type = TileType.FLOOR
};
}
public static Tile BuildFarmlandTile()
{
return new Tile {
IsWalkable = true,
IsBuildSpace = false,
ZoneType = Zone.Arable,
TileGraphicIndex = 2,
Type = TileType.FARMLAND
};
}
public enum Zone{
Shipping,
Receiving,
Arable,
None
}
public enum TileType{
WALL,
FLOOR,
FARMLAND
//...etc
}
}
Just extending on Diegos answer, the methods can just be fields for cleanliness
public class Tile{
public TileType Type { get; private set; }
public bool IsWalkable { get; private set; }
public bool IsBuildSpace { get; private set; }
public Zone ZoneType { get; private set; }
public int TileGraphicIndex { get; private set; }
private Tile() { }
public static Tile BuildTile(TileType type){
switch (type) {
case TileType.WALL: return BuildWallTile();
case TileType.FLOOR: return BuildFloorTile();
case TileType.FARMLAND: return BuildFarmlandTile();
default: throw ArgumentException("type");
}
}
public static Tile wall {
get {
return new Tile {
IsWalkable = false,
IsBuildSpace = false,
ZoneType = Zone.None,
TileGraphicIndex = 1,
Type = TileType.WALL
};
}
}
public static Tile floor {
get {
return new Tile {
IsWalkable = true,
IsBuildSpace = None,
ZoneType = Zone.None,
TileGraphicIndex = 1,
Type = TileType.FLOOR
};
}
}
public static Tile farmland {
get {
return new Tile {
IsWalkable = true,
IsBuildSpace = false,
ZoneType = Zone.Arable,
TileGraphicIndex = 2,
Type = TileType.FARMLAND
};
}
}
public enum Zone{
Shipping,
Receiving,
Arable,
None
}
public enum TileType{ WALL, FLOOR, FARMLAND //...etc }
}
Usage:
Tile myWallTile = Tile.wall;
Tile myFloorTile = Tile.floor;
I wanted to present a totally different (and self admittedly insane) approach from the many suggestions thus far. If you're willing to totally throw type safety out the window, consider this:
public interface IValueHolder
{
object Value {get; set;}
}
public class IsWalkable : Attribute, IValueHolder
{
public object Value {get; set;}
public IsWalkable(bool value)
{
Value = value;
}
}
public class IsBuildSpace : Attribute, IValueHolder
{
public object Value {get; set;}
public IsBuildSpace(bool value)
{
Value = value;
}
}
public enum Zone
{
None,
Arable,
}
public class ZoneType : Attribute, IValueHolder
{
public object Value {get; set;}
public ZoneType(Zone value)
{
Value = value;
}
}
public class TileGraphicIndex : Attribute, IValueHolder
{
public object Value {get; set;}
public TileGraphicIndex(int value)
{
Value = value;
}
}
public class TileAttributeCollector
{
protected readonly Dictionary<string, object> _attrs;
public object this[string key]
{
get
{
if (_attrs.ContainsKey(key)) return _attrs[key];
else return null;
}
set
{
if (_attrs.ContainsKey(key)) _attrs[key] = value;
else _attrs.Add(key, value);
}
}
public TileAttributeCollector()
{
_attrs = new Dictionary<string, object>();
Attribute[] attrs = Attribute.GetCustomAttributes(this.GetType());
foreach (Attribute attr in attrs)
{
IValueHolder vAttr = attr as IValueHolder;
if (vAttr != null)
{
this[vAttr.ToString()]= vAttr.Value;
}
}
}
}
[IsWalkable(true), IsBuildSpace(false), ZoneType(Zone.Arable), TileGraphicIndex(2)]
public class FarmTile : TileAttributeCollector
{
}
An example of usage:
FarmTile tile = new FarmTile();
// read, can be null.
var isWalkable = tile["IsWalkable"];
// write
tile["IsWalkable"] = false;
// add at runtime.
tile["Mom"]= "Ingrid Carlson of Norway";

Categories