I suggest and aprroach of plain-code generation similar to this: https://github.com/chismar/MyNetworkEngine
Roslyn reads files, parses them and allows you to generate code via text templating.
You'd need to generate component structs from interfaces.
Interfaces can define simple properties (any serializable value, be it int or Quaternion, with some way of adding new ones (an example of this can bee seen as "FastSerializerGetter" in my code)), references to other components or lists of components.
Those references and lists are implemented as other entities, which might have more components than just the one root mentioned in a ref or list.
RefToComponent -> struct RefToComponent -> Get where K : T -> GetEntityById -> GetComponent
used as MyEntity -> MyComponent -> RefToComponentValue.Get
where GeneratedInventoryStruct is
public interface IInventory
{
int ItemsCountCachedValueExample { get; set;}
RefToComponent<ISomeOtherNetworkedInterface> RefExample {get; set;}
DeltaList<ISomeElseNetworkedInterface> ListExample { get; set; }
}
public static InventoryRpcs
{
void RpcMyRpc<T>(this ref T inventory, int value) where T : IInventory
{
inventory.ItemsCountCachedValueExample = value;
}
}
----------------------another file--------------------
struct GeneratedInventoryStruct : IInventory (interface just consists of get/set values)
{
private int/byte/short mask;
int ItemsCountCachedValueExample { get; set;} // get => ... set => _value = value; mask |= 1 << %generated_prop_number%
RefToComponent<ISomeOtherNetworkedInterface> RefExample {get; set;}
DeltaList<ISomeElseNetworkedInterface> ListExample { get; set; } // the only diff betwee ref and list is that a list is essentially a list of RefToComponent in notion. Implemented via ICollectionElement or something, you know what I mean in DOTS.
//this RPCs are defined from extensions method in a specially named class near the original IInventory interface definition.
void RpcMyRpc(int value) => (world.???.Enqueue<RpcMyRpcMessageImplementation>(new RpcMyRpcMessageImplementation(value))); //then there should be a system that dispatches those messages as RPCs back, that assumes that there's an extension method for implementation
}
serialization/deserialization management is done via 2 generated systems, which will take a read write lock on all components and foreach entity will check if it has one or not. This isn't the most efficient way of doing it, but it can be optimized eventually, by caching per-archetype set of networked components.
Essentially, you generate a big enum and a big switch function, which takes an entity, gets component mentioned in enum, and serializes it. And then you have a system which, by default, iterates over enum calling the func over and over, to append data to a predefined buffer.
This system can be optimized, if you make it so that any actual write to a networked component instantiates an entity, with a single component that's referencing the dirty one. Thus, the serialization system can then iterate in parallel over all the dirty-marker-entities, get referenced original entity and serialize it, then destroy the marker-entity. It would be much faster than attaching component to the changed entity, as this incurs copying it to another buffer, while creating new simple one just appends to an archetype buffer.
----------------original discord text------------------------------
If we intend to work with structs only, PZ approach would work fine.
We've defined interfaces like
interface Inventory
{
int ItemCount;
DeltaList
Task RpcTryDropItem(NetObjectId itemId)
}
where Item is also an interface, with the same treatment, and methods are converted to RPCs
Then classes were generated, and implemenation of this interface is written by a programmer, with all the boilerplate in the partial class. We can't implement this interfaces here, as that won't really do much, as we can't pass them around, so this is essentially just a DSL.
While personally I prefer a different approach, as can be seen in https://github.com/chismar/MyNetworkEngine/tree/master/Yogollag (single-threaded version of this framework is being used in an actual production codebase :slight_smile: )
with code gen creating a child class by overriding virtual properties, it won't work with structs :frowning:
So, given an interface, you generate the actual structs you work with. This structs will be in the form of
struct Inventory
{
byte/short/int _dirtyMask
int _itemCount
int ItemCount { get => _itemCount set { Utility.SetField(ref _dirtyMask, ref _itemCount, 0) } }
}
Collections would be treated as those DOTS collections, where you define the component type. But adding/removing the elements should be done through, again, extension methods.
Any modification should result in a creation of a special single-component entity with the only property being the id of the creator, if such entity wasn't already created (defined by whether the value in the NetworkEntity component is set to true)
Those entities are what drives the network sync system, which goes for all of them and serializes for all those to whom they are replicated. If there's no one to replicate, it simply clears the mask. After it finishes, all the DirtyEntity'ies are cleared.
This allows us to no process all the entities all the time, even if they haven't changed.
Rpcs are generates as extension methods, which create a struct with all the parameters, passed and then executed in an ordered manner. If specified to be safe, they can be executed in parallel on the per-component or per-entity basis (meaning, we lock all the entities with specified components, but can process systems that don't require them, while processing the messages in a single thread, or we lock everything, but can process all the entities in parallel)
Code generation in both cases of PZ and mine home project were done with Roslyn and some variations of AArnot codegen. Although with modern .NET you can use native build toolchain, it's not available in Unity. Still, you can just make a button, that would launch a codegen exe :smile: Codegen works via roslyn
Github accidently lost my entire text here.
This is semi-TLDR version.
I suggest and aprroach of plain-code generation similar to this: https://github.com/chismar/MyNetworkEngine Roslyn reads files, parses them and allows you to generate code via text templating.
You'd need to generate component structs from interfaces. Interfaces can define simple properties (any serializable value, be it int or Quaternion, with some way of adding new ones (an example of this can bee seen as "FastSerializerGetter" in my code)), references to other components or lists of components.
Those references and lists are implemented as other entities, which might have more components than just the one root mentioned in a ref or list.
RefToComponent -> struct RefToComponent -> Get where K : T -> GetEntityById -> GetComponent
used as MyEntity -> MyComponent -> RefToComponentValue.Get
where GeneratedInventoryStruct is
serialization/deserialization management is done via 2 generated systems, which will take a read write lock on all components and foreach entity will check if it has one or not. This isn't the most efficient way of doing it, but it can be optimized eventually, by caching per-archetype set of networked components. Essentially, you generate a big enum and a big switch function, which takes an entity, gets component mentioned in enum, and serializes it. And then you have a system which, by default, iterates over enum calling the func over and over, to append data to a predefined buffer.
This system can be optimized, if you make it so that any actual write to a networked component instantiates an entity, with a single component that's referencing the dirty one. Thus, the serialization system can then iterate in parallel over all the dirty-marker-entities, get referenced original entity and serialize it, then destroy the marker-entity. It would be much faster than attaching component to the changed entity, as this incurs copying it to another buffer, while creating new simple one just appends to an archetype buffer.
----------------original discord text------------------------------ If we intend to work with structs only, PZ approach would work fine. We've defined interfaces like interface Inventory { int ItemCount; DeltaList-
Task
RpcTryDropItem(NetObjectId itemId)
}
where Item is also an interface, with the same treatment, and methods are converted to RPCs Then classes were generated, and implemenation of this interface is written by a programmer, with all the boilerplate in the partial class. We can't implement this interfaces here, as that won't really do much, as we can't pass them around, so this is essentially just a DSL.
While personally I prefer a different approach, as can be seen in https://github.com/chismar/MyNetworkEngine/tree/master/Yogollag (single-threaded version of this framework is being used in an actual production codebase :slight_smile: ) with code gen creating a child class by overriding virtual properties, it won't work with structs :frowning:
So, given an interface, you generate the actual structs you work with. This structs will be in the form of struct Inventory { byte/short/int _dirtyMask int _itemCount int ItemCount { get => _itemCount set { Utility.SetField(ref _dirtyMask, ref _itemCount, 0) } } }
Collections would be treated as those DOTS collections, where you define the component type. But adding/removing the elements should be done through, again, extension methods.
Any modification should result in a creation of a special single-component entity with the only property being the id of the creator, if such entity wasn't already created (defined by whether the value in the NetworkEntity component is set to true) Those entities are what drives the network sync system, which goes for all of them and serializes for all those to whom they are replicated. If there's no one to replicate, it simply clears the mask. After it finishes, all the DirtyEntity'ies are cleared. This allows us to no process all the entities all the time, even if they haven't changed.
Rpcs are generates as extension methods, which create a struct with all the parameters, passed and then executed in an ordered manner. If specified to be safe, they can be executed in parallel on the per-component or per-entity basis (meaning, we lock all the entities with specified components, but can process systems that don't require them, while processing the messages in a single thread, or we lock everything, but can process all the entities in parallel) Code generation in both cases of PZ and mine home project were done with Roslyn and some variations of AArnot codegen. Although with modern .NET you can use native build toolchain, it's not available in Unity. Still, you can just make a button, that would launch a codegen exe :smile: Codegen works via roslyn