GameResourceMap is attached to any players, works as their "Resources" pocket.
In UnitProfileSO we have a SerializedDIctionary works as a cost map for that unit when being spawned.
Each UnitProfileSO will be converted into IBufferElementData and attached to the Entity that can spawn Units.
The Costs dictionary can not be put into IBufferElementData, that is what i currently concern right now.
Any suggestion? May be my Design is not good.
Thanks for reading.
#Wanna have Collection inside IBufferElementData
1 messages · Page 1 of 1 (latest)
You should think twice before deciding to use managed components for your code won't be burstable.
If your map is reasonably small, contains just a few items (8 maybe?) you can just store them in a simple dynamic buffer and do a linear search for cost. Bursted linear search would be much faster.
There is another solution: store a giant NativeHashMap on a singleton entity somewhere and use it to look up your cost. Btw if the cost of each resource is different for each player then you should devise an ID strategy to differentiate each item in that giant hash map.
Which approach is better really depends on your specific usecase. It would be more helpful if you provide more information around the case.
For example, does each player have a different set of resource costs? How is this resource cost map initiated for each player? Is this map dynamic or static? means, do you know before hand every resource costs for a player?
tertles library has https://gitlab.com/tertle/com.bovinelabs.core/-/blob/master/Documentation~/DynamicHashMap.md (not used it personally)
Thanks, I will try this way later.
Yeah, that was my bad, Dictionary in the SO is OK for Input correct data but in ecs world, we should store it as a dynamic buffer
In my case, each player has their own 'cost scale' map (<UnitType, float Cost scale>), real cost of unit will be calculated by: cost scale * base cost. So the giant NativeHashMap you said stores all of my Units in the game and their cost (The number of unit may be around 20 - 30) so i don't think i need ID strategy
anyway, how NativeHashMap supposed to work in ICD? I tried both managed and unmanaged ICD
Just put NativeHashMap inside a wrapper class then put that wrapper into managed ICD.
Put the native hash map directly in an ICD, you need no class here.
can't use that in IJE
it's a singleton btw
you could map this through abstraction:
Unit has cost type.
Singleton has cost type -> cost map.
so now units don't need dynamic buffers or hash maps on them
instead you just use their type/id to get the costs
isn't that the giant native hash map I talked about 🤔
Tried, always got this exception:
ArgumentException: Blittable component type 'Components.GameResource.ResourceCostMapTest' on GameObject 'Player' contains a (potentially nested) pointer field. Serializing bare pointers will likely lead to runtime errors. Remove this field and consider serializing the data it points to another way such as by using a BlobAssetReference or a [Serializable] ISharedComponent. If for whatever reason the pointer field should in fact be serialized, add the [ChunkSerializable] attribute to your type to bypass this error.
Put NativeHashMap into a wrapper class and the exception is gone
As I understand, cost map is singleton managed component, inside that, there is a Dictionary<UnitType, Dictionary<CostType, Cost>>, each time anything need the cost of unit, just look it up in the giant Dictionary.
show your code please
note that, this native hash map must be created at runtime. You can't bake it into subscene, if this is what you're doing.
ok that may be the reason, i just tested it using baking
You can only use dynamic buffer or blob asset if you want to bake collections into subscene.
Native collections can't be baked.
If you want to bake the hash map as dynamic buffer, you can use this tool
unmanaged
Can i have HashMap in HashMap like what i would do with Dictionary<UnitType, Dictionary<CostType, Cost>> ?
and it's not nested dictionary inside other dictionary
it's one big flat dictionary
nested dictionary won't work well with job system + it's simply much slower than flat solution
That's why I told you to devise an ID strategy
you can make a complex ID out of these 2 components UnitType and CostType
I usually avoid enums, but you can make it enum if you want
I prefer to use guids, which are created from assets (scriptable object for example)
heard about it but i haven't try, will dive into that later on
what i understand from this:
Suppose UnitType: 0->3
CostType: 0->4
So the key would be combination of 2 number, no 2-dimensions collection, just one collection but with m*n elements?
I would do this for example. UnitCostId would be the key for any kind of hash map. Essentially it's just an ushort value.
public enum UnitType : byte { } // enough for 255 values
public enum CostType : byte { } // enough for 255 values
[StructLayout(LayoutKind.Explicit)]
public readonly struct UnitCostId : IEquatable<UnitCostId>
{
[FieldOffset(0)] private readonly ushort _raw;
[FieldOffset(0)] public readonly UnitType Unit;
[FieldOffset(1)] public readonly CostType Cost;
public UnitCostId(UnitType unit, CostType cost) : this()
{
Unit = unit;
Cost = cost;
}
public bool Equals(UnitCostId other)
=> _raw == other._raw;
public override bool Equals(object obj)
=> obj is UnitCostId other && _raw == other._raw;
public override int GetHashCode()
=> _raw.GetHashCode();
public static bool operator ==(UnitCostId left, UnitCostId right)
=> left._raw == right._raw;
public static bool operator !=(UnitCostId left, UnitCostId right)
=> left._raw != right._raw;
}
Another approach would be using an algorithm that makes 1D index out of an 2D index.
That would be less of a boilerplate than this
For me, in practice, I use source generator to make a complex ID out of multiple enums. Because I have many of these complex IDs to the point that manual coding is too tedious.
you mean '1' and '2' become key '12' ?
I would use a lager int type for storing those 2, if each of those 2 is 32 bytes, ulong 64 bytes to make combination of 2 enum would be fine.
I don't see you set the _raw value, this is what you said about "Source generator"? Do those Attributes as [FieldOffset...], ... do anything to that _raw? I haven't seen those attributes before
In essence, this ID type contains only 1 value field and that is the _raw. Both Unit and Cost fields are just a section in bytes on the byte-stream of _raw. In the constructor, you set the value for _raw just by setting each byte-section.
That means you don't have to set the value for _raw explicitly.
FieldOffset attribute is a tool to let the program know on which byte-section you want to store a specific value.
ushort is a 2 bytes. The first byte to store a value of UnitType, the second to store a value of CostType.
so if you have a pair UnitType=1 and CostType=2, then the layout in memory would be
_raw = 0b__0000_0001__0000_0010 in binary
( 1 ) ( 2 )
which is 0x12 in hexadecimal
which is 18 in decimal.
The sample code for UnitCostId above is a complete and functional code. You can copy it into your project to use immediately. You need nothing else.
Source generator is just a tool that helps me write the code for a lot of these complex IDs with less code. It has nothing to do with your question. Let's just forget about it for now.
My apologies for adding confusing information.
No problem, i will read the whole things when i have time :3 thanks for the detailed explaination
I just do some research of those "I Haven't seen" attributes and here is what i understand of your UnitCostId:
ushort _raw: 2 bytes = sum of both enum, _raw's bits are fully overrided by 2 following enum fields then _raw works as a mask to help me view the combination of 2 enums
Did i understand correctly?
Coming into this late, I think others have already made similar suggestions but without directly calling it out (maybe they did I didn't read the whole thread).
But the concept you have is very object oriented design, not data oriented design.
I find whenever I want to associated data with an object (like in your case the player) I need to ask myself, do I think of this like an object that owns that data and so I'm thinking about PlayerA's Resources versus PlayerB's Resources. If the answer is yes, I try and think of a way to manage the resources and access them without them knowing which Player they belong to.
Almost like Inversion of Control theory, so in this example, the Resource in question, shouldn't need to know about what Player it belongs to, all it's logic should be able to function without knowing which player it belongs to, or maybe even a scenario where there is no Player at all, or some other type of object that eventually might need Resources that you haven't planned for.
When you start to think of things this way, you stop organizing your data into Collections or Lists or Arrays that you can iterate over, and why would you, ECS already has Components that are organized into chunks, each chunk is an archetype that already acts like the container for your data anyway. By suggesting to put a collection inside a buffer, you're telling ecs you want to have a container of containers of containers that belong to the Player archetype.
While that's technically feasible, I wouldn't be surprised if you end up with poor chunk utilization or everything not fitting in chunks and just being pushed to the heap anyway causing you to completely lose out on the performance of dots.
You could have every Resource be an entity, every entry in your Costs dictionary be an entity, and their components can store references to the player entity they belong to.
Then you can make use of the SystemAPI.Query which is very fast to operate. Good luck.
I don't think it's a good idea to turn a lookup table into a group of entities. A lookup table exists so that you can search for a piece of data as fast as you can. Also the data contained in a lookup table is generally immutable. Entities imply the ability to change, and a group of entities certainly can't ensure the fastest search speed.
You shouldn't call it a "sum". To be exact, it's a "combination" of 2 components.