Emulate C-Sharp features using struct¶
What¶
Many C-Sharp features could be emulated using just struct
Why¶
- DOTS offer high performance
- But Burst-compatible code is limited in functionality
- OOP has lots of tools to reuse code:
- Inheritance, abstract and virtual method
- Enumerator
- Delegate / function pointer
- Emulating these features allow us to avoiding code repetition
How¶
Generic and interface as our tools¶
Struct¶
- Struct does not support inheritance
- Struct could implement interface
- Using struct as interface will cause boxing, i.e. an object is created to store the struct value
- Boxing will cause a compilation error for Burst
Generic¶
- To avoid boxing, we use generic
Emulate base class methods¶
class Base {
int BaseField1;
float BaseField2;
float Compute(float input) => input + BaseField1 + BaseField2;
}
class A: Base {
float2 FieldA;
}
class B: Base {
float3 FieldB;
}
struct Base: IComponentData {
int BaseField1;
float BaseField2;
float Compute(float input) => input + BaseField1 + BaseField2;
}
struct A: IComponentData {
float2 FieldA;
}
struct B: IComponentData {
float3 FieldB;
}
// entity1: component Base + A
// entity2: component Base + B
EntityManager.GetComponent<Base>(entity1).Compute(1);
struct Base {
int BaseField1;
float BaseField2;
float Compute(float input) => input + BaseField1 + BaseField2;
}
interface IBase {
Base BaseFields {get;}
}
struct A: IBase {
Base BaseFields {get; set;}
float2 FieldA;
}
struct B: IBase {
Base BaseFields {get; set;}
float3 FieldB;
}
void SomeMethod<T>(T value) where T:unmanaged, IBase {
value.BaseFields.Compute(1);
}
interface IBase {
int BaseField1 {get;}
float BaseField2 {get;}
}
struct A: IBase {
int BaseField1 {get; set;}
float BaseField2 {get; set;}
float2 FieldA;
}
struct B: IBase {
int BaseField1 {get; set;}
float BaseField2 {get; set;}
float3 FieldB;
}
static class BaseExtensions{
static float Compute<T>(this T value, int input)
where T:unmanaged, IBase
{
return input + value.BaseField1 + value.BaseField2;
}
}
A a;
a.Sum(1);
Emulate Method Override (abstract)¶
class Base {
int BaseField1;
float BaseField2;
abstract float Compute(int input);
}
class A: Base {
float2 FieldA;
override float Compute(int input){
return FieldA + input + BaseField1 + BaseField2;
}
}
class B: Base {
float3 FieldB;
override float Compute(int input){
return FieldB * input * BaseField1 * BaseField2;
}
}
Base obj;
obj.Compute(1);
interface IBase {
int BaseField1 {get;}
float BaseField2 {get;}
float Compute(int input);
}
struct A: IBase {
int BaseField1 {get; set;}
float BaseField2 {get; set;}
float2 FieldA;
float Compute(int input){
return FieldA + input + BaseField1 + BaseField2;
}
}
class B: IBase {
int BaseField1 {get; set;}
float BaseField2 {get; set;}
float3 FieldB;
override float Compute(int input){
return FieldB * input * BaseField1 * BaseField2;
}
}
void SomeMethod<T>(T value) where T:unmanaged, IBase {
value.Compute(1);
}
Emulate Method Override (virtual)¶
The previous method is not ideal if some struct want to use the base method
class Base {
int BaseField1;
float BaseField2;
virtual float Compute(int input) {
return input + BaseField1 + BaseField2;
}
}
class A: Base {
float2 FieldA;
override float Compute(int input){
return FieldA + input + BaseField1 + BaseField2;
}
}
class B: Base {
float3 FieldB;
}
Base obj = new B();
obj.Compute(1); // same implementation as Base.Compute
default implementation
of interfaces
interface IBase {
int BaseField1 {get;}
float BaseField2 {get;}
float Compute(int input) {
return input + BaseField1 + BaseField2;
}
}
struct A: IBase {
int BaseField1 {get; set;}
float BaseField2 {get; set;}
float2 FieldA;
float Compute(int input){
return FieldA + input + BaseField1 + BaseField2;
}
}
class B: IBase {
int BaseField1 {get; set;}
float BaseField2 {get; set;}
float3 FieldB;
}
void SomeMethod<T>(T value) where T:unmanaged, IBase {
value.Compute(1);
}
interface IBase {
float Compute() => 123; // default implementation
}
interface IA: IBase {
float ComputeA() => 111; // default implementation
}
struct A1: IA {
float Compute() => 456; // interface implementation
// inherit ComputeA()
}
struct A2: IA {
// inherit Compute()
float ComputeA() => 222; // interface implementation
}
static class AExtensions{
static float Calculate<T>(this T value) where T:unmanaged, IA {
return 789;
}
}
A1 a1;
a1.Compute(); // 456
a1.ComputeA(); // 111
a1.Calculate(); // 789
A2 a2;
a2.Compute(); // 123
a2.ComputeA(); // 222
a2.Calculate(); // 789
Emulate enumerator¶
IEnumerable<int> Compute(int input){
int val1 = f1(input);
yield return val1;
int val2 = f2(input, val1);
yield return val2;
}
float state;
foreach (var value in Compute(1)){
state = f3(state, value);
Show(state);
}
Producer Consumer
pattern to solve this scenario
interface IConsumer{
void Consume(int value);
}
void Compute<T>(T consumer, int input) where T:unmanaged, IConsumer {
int val1 = f1(input);
consumer.Consume(val1);
int val2 = f2(input, val1);
consumer.Consume(val2);
}
struct ShowConsumer: IConsumer{
float State;
void Consume(int value){
State = f3(State, value);
Show(State);
}
}
Compute(new ShowConsumer(), 1);
Emulate Function Pointer¶
interface IFunc{
float Compute(float a, float b);
}
struct FuncJob<T>: IJobParallelFor
where T:unmanaged, IFunc
{
NativeArray<float> ArrayA;
NativeArray<float> ArrayB;
T Func;
NativeArray<float> Results;
void Execute(int i) {
Results[i] = Func.Compute(ArrayA[i], ArrayB[i]);
}
}
struct Add: IFunc {
float Compute(float a, float b) => a + b;
}
struct Mul: IFunc {
float Offset;
float Compute(float a, float b) => a * b + Offset;
}
new FuncJob<Add> {
...
Func = new Add(),
}.ScheduleParallel();
new FuncJob<Mul> {
...
Func = new Mul{Offset = 1},
}.ScheduleParallel();
Unity has a way to use function pointer directly in Burst-code: https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/csharp-function-pointers.html
Using System and Job with generic¶
- Generic is not an equivalence to polymorphism
- Generic types need to be known at Compile time
- With Polymorphism, types are determined at Runtime
- To compensate for this
- All possible types should be declared in the source code
- All possible combination of types should be captured by queries
We could apply Generic further to reduce code repetition
// We want to perform Add or Mul on entities with this archetype // - InputA // - InputB // - Func (either Add or Mul component) // - Result new FuncJob<Add> { ... }.ScheduleParallel(SystemAPI.QueryBuilder() .WithAll<InputA, InputB, Result>() .WithAll<Add> .Build()); new FuncJob<Mul> { ... }.ScheduleParallel(SystemAPI.QueryBuilder() .WithAll<InputA, InputB, Result>() .WithAll<Mul> .Build());
struct FuncJobScheduler<T> where T:unmanaged, IFunc { EntityQuery Query; T Func; FuncJobScheduler(T func, EntityManager em) Func = func; Query = em.CreateEntityQuery( typeof(InputA), typeof(InputB), typeof(Result), typeof(T)); } void Schedule(inputs){ new FuncJob<T>{ inputs, Func = Func, }.ScheduleParallel(Query); } } struct MySystem: ISystemBase { FuncJobScheduler<Add> _add; FuncJobScheduler<Mul> _mul; void OnCreate(ref SystemState state){ _add = new(new Add(), state.EntityManager); _mul = new(new Mul{Offset = 1}, state.EntityManager); } void OnUpdate(ref SystemState state){ var inputs = ...; _add.Schedule(inputs); _mul.Schedule(inputs); } }
Retrieving value using ref¶
interface IConsumer{
void Consume(int value);
int Result {get;}
}
struct SumConsumer: IConsumer{
int Result {get; set;}
void Consume(int value){
Result += value;
}
}
struct MinConsumer: IConsumer{
int Result {get; set;}
void Consume(int value){
Result = math.min(Result, value);
}
}
void Aggregate<T>(ref T consumer, NativeArray<int> array)
where T: unmanaged, IConsumer {
foreach (var value in array){
consumer.Consume(value);
}
}
var sum = new SumConsumer();
Aggregate(ref sum, array);
print(sum.Result);
var min = new MinConsumer();
Aggregate(ref min, array);
print(min.Result);
Summary¶
- Using interface
- To have the same method signature with different implementations
- To have the same data inputs or outputs using properties
- Using generic
- To have the same Bursted code using said interfaces
- Extension method
- To have the same implementation for all structs
- Similar to base class method (not virtual, not abstract)
- Interface default implementation
- Similar to base class virtual method
- Reduce code repetition
- Put the similar part into a method
- Extract an interface for the different part
- Create different structs to implement the difference