Skip to content

Instantly share code, notes, and snippets.

@c0ffeeartc
Last active January 17, 2021 17:24
Show Gist options
  • Save c0ffeeartc/04d1c10c06104cd8371b0c45e991c62b to your computer and use it in GitHub Desktop.
Save c0ffeeartc/04d1c10c06104cd8371b0c45e991c62b to your computer and use it in GitHub Desktop.
EventDriven/SystemLess DestroyWith feature for Entitas ECS framework

Usage example:

// during init - subscribe every emit/subscribe context combination
contexts.SubscribeDestroyWith(
  contexts.gameC,
  contexts.gameC.GetGroup( GameCMatcher.AllOf( GameCMatcher.Destroy, GameCMatcher.DestroyWith_Emit ) ) );

// later add emit
var ent = contexts.gameC.CreateEntity(  );
ent.GetOrAddDestroyWithEmit( contexts );

// then add subscribe
ent2 = contexts.gameC.CreateEntity(  );
ent2.AddDestroyWith_Subscribe( ent.destroyWith_Emit.Value );

ent.isDestroy	= true;  // triggers `ent2.isDestroy = true;` in group event callback

It's possible to use only DestroyWith_Subscribe and IdComponent. I prefer having separate emit, subscribe, and counter components.

Later will add ability to subscribe to multiple emit entitities.

using System;
using Entitas;
using Entitas.CodeGeneration.Attributes;
[Context( Ids.Game )]
[Context(Ids.GameState)] // second context to force generation of EntityApiInterface
public class DestroyWith_Emit : IComponent
{
public Int32 Value;
}
[Context( Ids.GameState )]
[Unique]
public class DestroyWith_Counter : IComponent
{
public Int32 Value;
}
[Context( Ids.Game )]
[Context(Ids.GameState)] // second context to force generation of EntityApiInterface
public class DestroyWith_Subscribe : IComponent
{
[EntityIndex] public Int32 Value;
}
using System;
using Entitas;
public static class DestroyWithExtensions
{
private static Int32 NextDestroyWith_Counter( Contexts contexts )
{
var newCounter = contexts.gameStateC.destroyWith_Counter.Value + 1;
contexts.gameStateC.ReplaceDestroyWith_Counter( newCounter );
return newCounter;
}
public static Int32 GetOrAddDestroyWith_Emit( this GameCEntity ent, Contexts contexts )
{
if ( ent.hasDestroyWith_Emit )
{
return ent.destroyWith_Emit.Value;
}
var newCounter = NextDestroyWith_Counter( contexts );
ent.AddDestroyWith_Emit( newCounter );
return newCounter;
}
public static Int32 DestroyWith<TEntSub, TEntEmit> ( this TEntSub subscribe, Contexts contexts, TEntEmit emitEnt )
where TEntSub : IDestroyEntity, IDestroyWith_SubscribeEntity
where TEntEmit : IDestroyEntity, IDestroyWith_EmitEntity
{
if ( emitEnt == null )
{
Debug.Log( "emitEnt == null" );
subscribe.isDestroy = true;
return -1; // error
}
var emitValue = emitEnt.GetOrAddDestroyWith_Emit( contexts );
if ( emitEnt.isDestroy )
{
subscribe.isDestroy = true;
}
else
{
subscribe.ReplaceDestroyWith_Subscribe( emitValue );
}
return emitValue;
}
public static void SubscribeDestroyWith<TEntEmit, TEntSubscribe>(
this Contexts contexts,
IContext<TEntSubscribe> contextSubscribe,
IGroup<TEntEmit> groupEmit )
where TEntEmit : class, IEntity, IDestroyWith_EmitEntity
where TEntSubscribe : class, IEntity, IDestroyEntity, IDestroyWith_SubscribeEntity
{
groupEmit.OnEntityAdded += ( group, entity, index, component ) =>
{
var entIndex = contextSubscribe.GetEntityIndex( Contexts.DestroyWith_Subscribe );
var entSubscribers =
( (EntityIndex<TEntSubscribe, int>) entIndex ).GetEntities( entity.destroyWith_Emit.Value );
foreach ( var entSub in entSubscribers )
{
entSub.isDestroy = true;
}
};
}
}
using System;
using NUnit.Framework;
namespace Custom.Sources
{
[TestFixture]
public class TestDestroyWithFeature
{
private Contexts _contexts;
[SetUp]
public void SetUp( )
{
_contexts = new Contexts( );
_contexts.SubscribeId( );
}
[TestCase( 1, 2, false )]
[TestCase( 19, 19, true )]
[TestCase( -1, -1, false )]
[TestCase( 1, -1, false )]
[TestCase( -1, 1, false )]
public void Test_DestroyWith( Int32 emitId, Int32 subscribeId, Boolean expected )
{
// given
_contexts.SubscribeDestroyWith( _contexts.gameC,
_contexts.gameC.GetGroup( GameCMatcher.AllOf( GameCMatcher.Destroy, GameCMatcher.DestroyWith_Emit ) ) );
var ent = _contexts.gameC.CreateEntity( );
if ( emitId >= 0 )
{
ent.AddDestroyWith_Emit( emitId );
}
var entSubscribe = _contexts.gameC.CreateEntity( );
if ( subscribeId >= 0 )
{
entSubscribe.AddDestroyWith_Subscribe( subscribeId );
}
// when
ent.isDestroy = true;
// then
Assert.AreEqual( expected, entSubscribe.isDestroy );
}
[Test]
public void Test_DestroyWith_Chain( )
{
// given
_contexts.SubscribeDestroyWith( _contexts.gameC,
_contexts.gameC.GetGroup( GameCMatcher.AllOf( GameCMatcher.Destroy, GameCMatcher.DestroyWith_Emit ) ) );
var emitId = 100;
var ent = _contexts.gameC.CreateEntity( );
ent.AddDestroyWith_Emit( emitId );
var entSubscribe = _contexts.gameC.CreateEntity( );
entSubscribe.AddDestroyWith_Subscribe( emitId );
var emitId2 = 122;
entSubscribe.AddDestroyWith_Emit( emitId2 );
var entSubscribe2 = _contexts.gameC.CreateEntity( );
entSubscribe2.AddDestroyWith_Subscribe( emitId2 );
// when
ent.isDestroy = true;
// then
Assert.AreEqual( true, entSubscribe.isDestroy );
Assert.AreEqual( true, entSubscribe2.isDestroy );
}
[Test]
public void Test_DestroyWith_CircularDependency( )
{
// given
_contexts.SubscribeDestroyWith( _contexts.gameC,
_contexts.gameC.GetGroup( GameCMatcher.AllOf( GameCMatcher.Destroy, GameCMatcher.DestroyWith_Emit ) ) );
var ent = _contexts.gameC.CreateEntity( );
var entSubscribe = _contexts.gameC.CreateEntity( );
var emitId = 100;
ent.AddDestroyWith_Emit( emitId );
entSubscribe.AddDestroyWith_Subscribe( emitId );
var emitId2 = 122;
entSubscribe.AddDestroyWith_Emit( emitId2 );
ent.AddDestroyWith_Subscribe( emitId2 );
// when
ent.isDestroy = true;
// then
Assert.AreEqual( true, ent.isDestroy );
Assert.AreEqual( true, entSubscribe.isDestroy );
}
[Test]
public void Test_DestroyWith_CircularDependency2( )
{
// given
_contexts.SubscribeDestroyWith( _contexts.gameC,
_contexts.gameC.GetGroup( GameCMatcher.AllOf( GameCMatcher.Destroy, GameCMatcher.DestroyWith_Emit ) ) );
var ent = _contexts.gameC.CreateEntity( );
var entSubscribe = _contexts.gameC.CreateEntity( );
var emitId = 100;
ent.AddDestroyWith_Emit( emitId );
entSubscribe.AddDestroyWith_Subscribe( emitId );
var emitId2 = 122;
entSubscribe.AddDestroyWith_Emit( emitId2 );
ent.AddDestroyWith_Subscribe( emitId2 );
// when
entSubscribe.isDestroy = true;
// then
Assert.AreEqual( true, ent.isDestroy );
Assert.AreEqual( true, entSubscribe.isDestroy );
}
[Test]
public void Test_DestroyWith_CircularDependency_Chain( )
{
// given
_contexts.SubscribeDestroyWith( _contexts.gameC,
_contexts.gameC.GetGroup( GameCMatcher.AllOf( GameCMatcher.Destroy, GameCMatcher.DestroyWith_Emit ) ) );
var ent = _contexts.gameC.CreateEntity( );
var entSubscribe = _contexts.gameC.CreateEntity( );
var entSubscribe2 = _contexts.gameC.CreateEntity( );
var emitId = 100;
ent.AddDestroyWith_Emit( emitId );
entSubscribe.AddDestroyWith_Subscribe( emitId );
entSubscribe2.AddDestroyWith_Subscribe( emitId );
var emitId2 = 122;
entSubscribe2.AddDestroyWith_Emit( emitId2 );
ent.AddDestroyWith_Subscribe( emitId2 );
// when
entSubscribe2.isDestroy = true;
// then
Assert.AreEqual( true, ent.isDestroy );
Assert.AreEqual( true, entSubscribe.isDestroy );
Assert.AreEqual( true, entSubscribe2.isDestroy );
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment