Problem with List of inherited objects

Started by
16 comments, last by Stefan Fischlschweiger 9 years, 5 months ago

In the game engine I'm working on I'm using an archtype system for different object types.

For this, I've implemented a base Archtype class that holds common values and inherited classes for specific archtypes like ships or weapons.

On startup, the engine loads all archtypes from .ini files into a List<Archtype> using Json

In various places I need to use the values of an archtype to load values for e.g. creating an Entity in the ECS

I retrieve archtypes from the List using this code:


internal Archtype GetArchtype(string nickname)
{
    var arch = Archtypes.Find(x => x.Nickname == nickname);

    return arch;
}

Now, the problem is that I can only access the properties present in the base Archtype class when using the returned value.

How can I access all properties?

So far I tried:

Type casting (giving me a "redundant type case" notice from ReSharper)

Setting the return type to object instead of Archtype (was contraproductive, couldn't access even the base properties)

Using ArrayList instead of Array (doesn't have a Find method)

EDIT. Problem solved =)

I split up retrieving the data of the archtype into 2 functions.

First I only get its Type using:


internal Type GetArchtype(string nickname)
{
    var arch = Archtypes.Find(x => x.Nickname == nickname).GetType();

    return arch;
}

Then I feed the Type into this generic function to retrieve the actual archtype:


internal T LoadArchtype<T>(string nickname)
{
    object arch = Archtypes.Find(x => x.Nickname == nickname);

    return (T) arch;
}
Advertisement

I don't know your usage patterns, but if all you need the System.Type for casting, do it in a single pass.

Also, combining both functions, as you've provided, won't work. Generics require you to have a concrete type at compile time, and System.Type represents type at run-time (unless you want to specialize for System.Type). You can use Type.MakeGeneric, or rather, MethodInfo.MakeGenericMethod to call your LoadArchtype<T> method via reflection with System.Type, but I doubt you aim for that.

Apart from that, once you have an instance of System.Type, you have to have a way of creating an instance from it. You can either use a factory method pattern, the prototype pattern (what you seem to be doing), one of the many Activator.CreateInstance overloads, or the evil FormatterServices.GetUninitializedObject.

Why use an ArrayList? Dictionary<string, Archetype> would seem more appropriate here.

Also, this may not be a particularly great use of inheritance. Inheritance is appropriate when the derived classes share their interface with the base class. However this doesn't seem to be the case here, which is precisely why you're having this problem. I'd be inclined to flatten the hierarchy, kill all the subclasses and turn Archetype into a generic grab-bag of properties which can contain appropriate values for any objects in your game.

First of all the word is "archetype". Pedantic, I know, but code is communication and concepts matter.

As Sandman said, inheritance is probably not what you want here. Anytime you have a collection of Base and you find yourself wanting to access Derived.Member when iterating, that is a code smell. Either refactor the interface to Base, or rethink your structure.

If you must have this structure, consider inverting the responsibility, i.e.


public class Archetype
{
    public int SomeProperty {get;set;}
    public int AnotherProperty {get;set;}

    public virtual void Populate(Entity e)
    {
        e.Foo = SomeProperty;
        e.Bar = AnotherProperty;
    }
}

public class EnemyArchetype : Archetype
{
    public string EnemyProperty {get;set;}

    public override void Populate(Entity e)
    {
        base.Populate(e)
        e.Baz = EnemyProperty;
    }
}

but I'd still recommend Sandmans approach

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight

My reason for inheritance was to store all different archtypes I have in a single list. (The name archtype was taken from the game Freelancer where an archtype is sort of a template for a ship, station, planet, gun, whatever).

e.g. I have a file called weapon_arch.ini which contains archtype information like this (actually in Json notation)

nickname = tr_laser01

model = Equipment\\Weapons\\tr_laser

damage = 25

etc.

On game startup this file (and others containing sorts of archtypes) are loaded and deserialized using Json.net

for each archtype in the files an appropriate xxxArchtype instance is created and filled with that data, then stored in a List<Archtype>

When creating an entity like a ship or gun or whatever the archtype information is looked up by nickname.

This information is the used to initialize the entity.

Say e.g. I want to create the player ship along with its equipment when loading a savegame

First I load in the savegame (also Json)

The savegame contains a Dictionary<string, string> with the Key representing a Hardpoint on the ship model and the Value representing the actual equipment mounted there.

What the savegame doesn't contain is what type of equipment a single item is (gun, shield, etc..)

therefore I first need to find out (by it's nickname) what category it is (with the first function), so I can use that information to create the correct type of entity and load the archtype values into it.

You can see this here in this function:


public static void LoadEquip(Dictionary<string, string> saveEquip, ulong parentId)
        {
            foreach (KeyValuePair<string, string> equip in saveEquip)
            {
                var arch = GameServices.Equip.GetArchtype(equip.Value);

                if (arch == typeof(WeaponArch))
                {
                    var weapon = GameServices.Equip.LoadArchtype<WeaponArch>(equip.Value);
                    var entity = GameServices.EntityManager.Create<EntityWeapon>();
                    var parent = GameServices.EntityManager.GetEntity(parentId);
                    entity.Hardpoint.Hardpoint = equip.Key;
                    Vector3 off;
                    HardpointHelper.GetHardpointOffset(parent.GetComponent<RenderComponent>().Model, equip.Key,
                        out off);
                    entity.Offset.Offset = off;
                    entity.Parent.ParentID = parent.Id;
                    entity.Position.Position = parent.GetComponent<PositionComponent>().Position + off;
                    entity.Rotation.Rotation = parent.GetComponent<RotationComponent>().Rotation;
                    Texture2D[] textures;
                    entity.Render.Model = ModelLoader.LoadModel(weapon.Model, GameplayScreen.Effect,
                        out textures);
                    entity.Render.Textures = textures;
                }
                else if (arch == typeof (ShieldArch))
                {
                    var shield = GameServices.Equip.LoadArchtype<ShieldArch>(equip.Value);
                    var entity = GameServices.EntityManager.Create<EntityShield>();
                    entity.Parent.ParentID = parentId;
                    entity.Shield.Capacity = shield.Capacity;
                    entity.Shield.ChargeRate = shield.RechargeRate;
                    entity.PowerDraw.PowerDraw = shield.PowerDraw;
                }
                else if (arch == typeof (EngineArch))
                {
                    var engine = GameServices.Equip.LoadArchtype<EngineArch>(equip.Value);
                    var entity = GameServices.EntityManager.Create<EntityEngine>();
                    entity.Parent.ParentID = parentId;
                    entity.Engine.Force = engine.Force;
                    entity.Engine.CruiseMul = engine.CruiseMultiplier;
                }
                else if (arch == typeof (PowerplantArch))
                {
                    var power = GameServices.Equip.LoadArchtype<PowerplantArch>(equip.Value);
                }
                else if (arch == typeof (CapacitorArch))
                {
                    var cap = GameServices.Equip.LoadArchtype<CapacitorArch>(equip.Value);
                }
            }  
        }
    }

The Archtype handling is contained in this class:


public class EquipArchManager
    {
        public enum WeaponType
        {
            Gun,
            Missile,
            Torpedo,
            Mine,
            Countermeasure
        }

        internal List<Archtype> Archtypes = new List<Archtype>();        

        public EquipArchManager()
        {
            // Load Weapon Archtypes.

            var weaponArchesRaw = string.Empty;

            try
            {
                weaponArchesRaw = File.ReadAllText(@"Content\Equipment\weapon_equip.ini");
            }
            catch (FileNotFoundException e)
            {
                LogFileWriter.WriteToLog(LogFileWriter.MessageType.Error, e.FileName + " missing!");
                MessageBox.Show("weapon_equip.ini not found");
                Environment.Exit(1);
            }            

            Archtypes.AddRange(JsonConvert.DeserializeObject<List<WeaponArch>>(weaponArchesRaw));

            // Load Shield Archtypes.

            var shieldEquipRaw = string.Empty;

            try
            {
                shieldEquipRaw = File.ReadAllText(@"Content\Equipment\shield_equip.ini");
            }
            catch (FileNotFoundException e)
            {
                LogFileWriter.WriteToLog(LogFileWriter.MessageType.Error, e.FileName + " missing!");
                MessageBox.Show("shield_equip.ini not found");
                Environment.Exit(1);
            }

            if (shieldEquipRaw != string.Empty)
                Archtypes.AddRange(JsonConvert.DeserializeObject<List<ShieldArch>>(shieldEquipRaw));
        }

        /// <summary>
        /// Get <see cref="Type"/> of derived archtype.
        /// </summary>
        /// <param name="nickname">Archtype nickname.</param>
        /// <returns>Type</returns>
        internal Type GetArchtype(string nickname)
        {
            var arch = Archtypes.Find(x => x.Nickname == nickname).GetType();

            return arch;
        }

        /// <summary>
        /// Load Archtype from List.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="nickname"></param>
        /// <returns>Derived archtype</returns>
        internal T LoadArchtype<T>(string nickname)
        {
            object arch = Archtypes.Find(x => x.Nickname == nickname);

            return (T) arch;
        }
    }

If you have any more questions about my code feel free to ask =)

The name archtype was taken from the game Freelancer where an archtype is sort of a template for a ship, station, planet, gun, whatever


Yes and the word is still "archetype" smile.png

Looking at your code, a few constructive criticisms (please take in the spirit they are intended)

public static void LoadEquip(Dictionary<string, string> saveEquip, ulong parentId)
// becomes
public static void LoadEquipment(IDictionary<string, string> savedEquipment, ulong parentId)
No need to shorten to LoadEquip. Call it LoadEquipment and let your IDE complete it for you.
In general, program to an interface rather than a concrete class.
"saveEquip" sounds like a verb (method, delegate) rather than a noun (variable, data, etc).

            foreach (KeyValuePair<string, string> equip in saveEquip)
            {
                var arch = GameServices.Equip.GetArchtype(equip.Value);
This is a personal preference, but I would argue there is no need to specify the exact type of equip. You're clearly ok with using var, so you may as well use it in the foreach loop too. Remember, the best code is the code you don't have to write.
                if (arch == typeof(WeaponArch))
                {
                    var weapon = GameServices.Equip.LoadArchtype<WeaponArch>(equip.Value);
                    // weapon stuff
                }
                else if (arch == typeof (ShieldArch))
                {
                    var shield = GameServices.Equip.LoadArchtype<ShieldArch>(equip.Value);
                    // shield stuff
                }
                else if (arch == typeof (EngineArch))
                {
                    // engine stuff
                }
                // etc.
If you see code like this, massive alarm bells should be ringing. LoadEquipment suddenly has to know how to load every different entity you have in the game.
Basically, don't do this.

Here's a different way to do this.
    public abstract class Archetype
    {
        public string Nickname { get; set; }

        public abstract void BuildEntity(string entityKey, ulong parentId);

    }

    public class WeaponArchetype : Archetype
    {
        public override void BuildEntity(string entityKey, ulong parentId)
        {
           var entity = GameServices.EntityManager.Create<EntityWeapon>();
            var parent = GameServices.EntityManager.GetEntity(parentId);
            entity.Hardpoint.Hardpoint = entityKey;
            Vector3 off;
            HardpointHelper.GetHardpointOffset(parent.GetComponent<RenderComponent>().Model, entityKey,
                out off);
            entity.Offset.Offset = off;
            entity.Parent.ParentID = parent.Id;
            entity.Position.Position = parent.GetComponent<PositionComponent>().Position + off;
            entity.Rotation.Rotation = parent.GetComponent<RotationComponent>().Rotation;
            Texture2D[] textures;
            entity.Render.Model = ModelLoader.LoadModel(Model, GameplayScreen.Effect,
                out textures);
            entity.Render.Textures = textures;
        }

        protected string Model { get; set; }
    }

    public class ShieldArchetype : Archetype
    {
        public override void BuildEntity(string entityKey, ulong parentId)
        {
            var entity = GameServices.EntityManager.Create<EntityShield>();
            entity.Parent.ParentID = parentId;
            entity.Shield.Capacity = Capacity;
            entity.Shield.ChargeRate = RechargeRate;
            entity.PowerDraw.PowerDraw = PowerDraw;
        }

        protected int PowerDraw { get; set; }

        protected int RechargeRate { get; set; }

        protected int Capacity { get; set; }
    }

    public class EquipmentArchetypeManager
    {
        public enum WeaponType
        {
            Gun,
            Missile,
            Torpedo,
            Mine,
            Countermeasure
        }

        internal Dictionary<string, Archetype> Archetypes = new Dictionary<string, Archetype>();

        public EquipmentArchetypeManager()
        {

            // Load Weapon Archetypes.
            var weaponArchetypesRaw = string.Empty;
 
            // loading weapon stuff, etc
            ///.... 
            JsonConvert.DeserializeObject<List<WeaponArchetype>>(weaponArchetypesRaw)
                .ForEach(archetype => Archetypes.Add(archetype.Nickname, archetype));
           
            // Load shield Archetypes.
            var shieldArchetypesRaw = string.Empty;
 
            // loading weapon stuff, etc
            ///.... 
            JsonConvert.DeserializeObject<List<ShieldArchetype>>(shieldArchetypesRaw)
                .ForEach(archetype => Archetypes.Add(archetype.Nickname, archetype));
 
} 
public void LoadEquipment(IDictionary<string, string> savedEquipment, ulong parentId) { foreach (var equipment in savedEquipment) { Archetypes[equipment.Value].BuildEntity(equipment.Key, parentId); } } }

Advantages:
  • dictionary lookup for your archetypes
  • separation of concerns
  • can add new archetypes without needing to change LoadEquipment
Now, this is really just a first step. A better system again would be to follow Sandmans advice and flatten the hierarchy. Then you can start to move towards an entity system.

HTH
if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight

I already thought about using a dictionary instead of a list to store the archetypes. I just didn't know before how I could split the nickname from the values =)

Also, I'm already using an Entity Component System in my game. But I keep the archetype handling seperate from it.

I'll try to modify my code according to your advice.

Hey Chaos, thanks for your help, that code change will save me a lot of hassle (and code)

Hey Chaos, thanks for your help, that code change will save me a lot of hassle (and code)

No problem, man.... glad I could help.

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight

One last question though: What would be the best approach if I'd need different BuildEntity methods (with different arguments) ?

This topic is closed to new replies.

Advertisement