Is it possible to group various components? Maybe with an interface?
public struct Energy : IComponentData, IStat```
How would I get an entity's components that are IStat? Could I do this within a job?
Is it possible to group various components? Maybe with an interface?
public struct Energy : IComponentData, IStat```
How would I get an entity's components that are IStat? Could I do this within a job?
Perhaps you can have a separate entity that only has IStat components, and reference this entity from the main entity. You could do this recursively for other “interfaces” like:
Character
Use ComponentDataFromEntity(Entity entity) to fetch components inside of a job.
var offenseEntity = characterEntity.statsEntity.offenseEntity;
var fireballComponent = getFireballFromEntity[offenseEntity];
var lightningComponent = getLightningFromEntity[offenseEntity];
var iceballComponent = getIceballFromEntity[offenseEntity];
// where FireBall, Lightning, and IceBall would have the IOffense interface following your example
If you’re looking for another way, EntityManager has
public NativeArray<ComponentType> GetComponentTypes(Entity entity, Allocator allocator = null)
but it cannot be used inside of a job.
EntityCommandBuffer can be used in a job but it cannot be used to fetch components - it’s used to schedule modification of components to a later time.
While you can’t query the interface you can still use them to do some cool things and save yourself writing a bunch of duplicate code.
For example I used reusable jobs for an AI solution.
(Please note I’m pulling this from an older repo as an example, it’s using a previous version of entities so still uses IJobProcessComponentDataWithEntity)
The interface looked like this
public interface ITargetOrder : IComponentData
{
/// <summary>
/// Gets or sets the target of the order.
/// </summary>
Entity Target { get; set; }
/// <summary>
/// Gets or sets if the order is processing? Usually happens after being in range of target.
/// </summary>
bool IsProcessing { get; set; }
/// <summary>
/// Gets or sets the last time the order started processing.
/// </summary>
float StartTime { get; set; }
}
Implemented like this for example
/// <summary>
/// The deposit order which tells an entity to empty their inventory into a storage.
/// </summary>
public struct DepositOrder : ITargetOrder
{
/// <summary>
/// Gets the target storage.
/// </summary>
public Entity Storage;
/// <summary>
/// Gets if we are currently depositing.
/// </summary>
public bool IsDepositing;
/// <summary>
/// Gets the start time of the depositing routine.
/// </summary>
public float StartTime;
/// <inheritdoc />
Entity ITargetOrder.Target
{
get => this.Storage;
set => this.Storage = value;
}
/// <inheritdoc />
bool ITargetOrder.IsProcessing
{
get => this.IsDepositing;
set => this.IsDepositing = value;
}
/// <inheritdoc />
float ITargetOrder.StartTime
{
get => this.StartTime;
set => this.StartTime = value;
}
}
And the job looks like this
[BurstCompile]
public struct MoveToTargetJob<TO> : IJobProcessComponentDataWithEntity<TO, Translation>
where TO : struct, ITargetOrder
{
public void Execute(Entity entity, int index, ref TO order, [ReadOnly] ref Translation position)
{
var target = order.Target;
if (target == Entity.Null)
{
return;
}
// ... etc
So all my orders that needed to move to a target implemented the ITargetOrder interface then I could just re-use this job in multiple different systems that handled different logic routines.
Is it possible to loop over an entity’s components, or get the full archetype of the entity and loop over the respective component types? If so I might be able to pull out the ones that implement iStat.
I hacked together a solution, but I’m sure there’s a better way.
Scripts
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Unity.Entities;
using UnityEngine;
public interface IStat
{
}
[Serializable]
public struct Hunger : IComponentData, IStat
{
public float Value;
}
[Serializable]
public struct Health : IComponentData, IStat
{
public float Value;
}
public class TestSystem : ComponentSystem
{
EntityQuery IStatComponents;
protected override void OnCreate()
{
StatComponentsQueryViaInterface();
}
protected override void OnUpdate()
{
}
void StatComponentsQueryViaInterface()
{
Type istat = typeof(IStat);
List<Type> statTypes = new List<Type>();
foreach (Type t in Assembly.GetExecutingAssembly().GetTypes())
{
foreach(Type i in t.GetInterfaces())
{
if (i == istat)
{
statTypes.Add(t);
}
}
}
TypeListToEntityQuery(statTypes);
}
void TypeListToEntityQuery(List<Type> types)
{
List<ComponentType> cts = new List<ComponentType>();
foreach(Type t in types)
{
cts.Add( new ComponentType(t, ComponentType.AccessMode.ReadWrite) );
}
IStatComponents = GetEntityQuery(new EntityQueryDesc
{
Any = cts.ToArray()
});
}
}
edit: cleaned up code a bit.
I am sure interface with struct component data should be avoid if possible since the idea of grouping unknown data (same as polymorphism in OOP but in Data type) to query system is not very nice. Because the system still have to know what kind of Data that is to process while you still have to setup Data structure manually not dynamically in Editor.
I would write [HeaderAttribute] instead of interface to group stuff with reflection during run time.
Fair point. Here’s the attribute based implementation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Unity.Entities;
using UnityEngine;
public class Stat : Attribute
{
}
[Serializable] [Stat]
public struct Hunger : IComponentData
{
public float Value;
}
[Serializable] [Stat]
public struct Health : IComponentData
{
public float Value;
}
public class TestSystem : ComponentSystem
{
EntityQuery IStatComponents;
protected override void OnCreate()
{
StatComponentsQueryViaAttribute();
}
protected override void OnUpdate()
{
}
void StatComponentsQueryViaAttribute()
{
Type stat = typeof(Stat);
List<Type> statTypes = new List<Type>();
foreach(Type t in Assembly.GetExecutingAssembly().GetTypes()) {
if (t.GetCustomAttributes(stat, true).Length > 0) {
statTypes.Add(t);
}
}
TypeListToEntityQuery(statTypes);
}
void TypeListToEntityQuery(List<Type> types)
{
List<ComponentType> cts = new List<ComponentType>();
foreach(Type t in types)
{
cts.Add( new ComponentType(t, ComponentType.AccessMode.ReadWrite) );
}
IStatComponents = GetEntityQuery(new EntityQueryDesc
{
Any = cts.ToArray()
});
}
}
Did you guys miss tertle’s solution?
This is 1,000,000 times better than the reflection you guys are doing, please anyone else that sees this thread, please please please do not use reflection during the runtime of your game. I would like the games that you guys create in the future to not be laggy games because of reflection, if you need an example using ComponentSystem instead of JobComponentSystem here it is:
public interface IStat : IComponentData
{
float Value { get; }
}
public struct StrengthData : IStat
{
public float Value;
float IStat.Value => Value;
}
public struct IntelligenceData : IStat
{
public float Value;
float IStat.Value => Value;
}
public struct AgilityData : IStat
{
public float Value;
float IStat.Value => Value;
}
public class StatSystem : ComponentSystem
{
protected override void OnCreate()
{
// If you need to do a query here's how it would work
GetEntityQuery(new EntityQueryDesc {
Any = new [] {
ComponentType.ReadWrite<StrengthData>(),
ComponentType.ReadWrite<AgilityData>(),
ComponentType.ReadWrite<IntelligenceData>(),
}
});
}
// If you want to use Entities.ForEach I would recommend doing this way to reuse code!
protected override void OnUpdate()
{
ForEachStat<StrengthData>();
ForEachStat<AgilityData>();
ForEachStat<IntelligenceData>();
}
protected void ForEachStat<TStat>() where TStat : struct, IStat
{
Entities.ForEach((ref TStat stat) => {
Debug.Log($"Stat {typeof(TStat)}: {stat.Value}");
});
}
}
}
But as you can see its much cleaner than the hack that was being presented up above and is much more performant as well. Also thank you tertle for showing me this, I was also wondering how this would be done which is how I stumbled upon this thread
Tertle’s solution is excellent if you know which component types implement IStat. I’m not sure how the solution works for an unknown number of IStat components (mods). I haven’t seem much on the forums about how modding will work with ecs, so I’ve been writing little projects that answer “Can I do this?” just to see where some limitations are.
var chunk = EntityManager.GetChunk(entity);
var compTypes = chunk.Archetype.GetComponentTypes(Allocator.TempJob);
Also a few words on Reflection. Right now the ECS api is so young, you cannot do anything custom without reflection. You need to do custom/multiple worlds? You need reflection. You want to do Netcode that sync components in a generic way? You need reflection.
I just finished my own Netcode. When it comes to reflection, key is to do as much as possible as one-off operations such as in OnCreate(). For performance-critical code in OnUpdate(), you need to convert all Method.Invoke() or equivalents to delegates. You can expect 20x to 40x performance gain this way (very close to calling the method directly). When dealing with EntityManager.AddComponent / GetComponent(), you also need to take care of boxing.
I don’t see anything wrong in the Reflection code in this thread. They are in OnCreate, so needn’t be performance-critical. And they can potentially serve very different purposes than just utilizing generics (tertle’s code).
No you don’t need. Read the manual.
https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/system_update_order.html
But in general, reflection it’s nothing bad, yes, if it in right hands.
As the API stands right now, if you are doing multiple worlds in any non-trivial manner, you will need to roll your own World Initialization code instead of using DefaultWorldInitialization as it. This is mostly due to 2 reasons:
Both GameObjectEntity and the new Conversion API depends on World.Active. And ICustomBootstrap doesn’t have a world passed in. So if you have multiple worlds and want to use ICustomBootstrap, you are forced to check World.Active. And if you’re doing multiple worlds, you’ll soon realize that having code dependent on checking a global static makes things error-prone and harder to test.
ScriptBehaviourUpdateOrder.UpdatePlayerLoop() doesn’t support multiple worlds now. Good news is they are getting rid of it completely in the future. But as it stands right now, a lot of things still depend on it (eg. Entity Debugger).
More context:
https://discussions.unity.com/t/734379
https://discussions.unity.com/t/734664
I know API, how it work and evolve from beginning. I use multiple wolds expensively, for different parts of logic, for asynch entities creation by ExclusiveEntityTransaction, for dividing presentation and simulation, etc.
You shouldn’t touch
ScriptBehaviourUpdateOrder manually as before, it’s wrong way in current API state. And this no need multiple worlds argument, coz how player loop works now, and how we build player loop now by filling update order of groups. Read manual which I linked, especially multiple world part, where described how properly work with multiple worlds.
ScriptBehaviourUpdateOrder.UpdatePlayerLoop() has the 3 top-level SystemGroups hardcoded in. So if you want to do anything custom (and have things show up in Entity Debugger), you need to resort to reflection or changing the source code.
I’m aware of the current “recommended” way of creating multiple worlds. But it’s limited. I don’t want to strictly adhere to the 3 set-in-stone SystemGroups for all my worlds.
My original reflection comment was really more about customized worlds than just multiple worlds.
Hah, I was about to chime in on the above until I saw the Entity Debugger comment. I’ve definitely just been living with systems not showing up there.
That said, I have had good success using the conversion APIs directly. For example, loading the same scene into two separate Worlds:
if (Settings.client) { // true
DefaultWorldInitialization.Initialize(WorldKey.CLIENT_WORLD.ToString(), false);
Worlds.clientWorld = World.Active;
GameObjectConversionUtility.ConvertScene(scene, default, Worlds.clientWorld);
}
if (Settings.server) { // true
DefaultWorldInitialization.Initialize(WorldKey.SERVER_WORLD.ToString(), false);
Worlds.serverWorld = World.Active;
GameObjectConversionUtility.ConvertScene(scene, default, Worlds.serverWorld);
}
Yep I agree with you in this part, if you need some “custom” toppest level groups. But what your case for root groups, if you always can put some other your own groups inside and onther inside them etc.
Yeah, in my case, I needed to use custom UpdateLoops for Netcode and Physics. The current way the root SystemGroups are set up doesn’t allow that. But I’m hopeful they’ll make it better in the next couple versions.
@Piefayth I think I did something similar, sort of bootstrapping client and server separately so that the conversion utility can be aware of which world to act on in Editor mode.