What is a good way of reproducing the functionality of a list of abstract classes or interfaces that process data in a bursted job equivalent?
A good example to explain: Pathfinding.
A Tile (scriptable object) is made to be composeable and has an IPathCostCalculator that gets called for calculating the cost for the current entity and the current tile. Implementations are for example: ConstantPathCost, HealthPathCost (the higher the entity hp, the lower the cost) or even math ones such as SumPathCost (adds the cost of n IPathCostCalculators together) and others.
Now for doing something like this in burst is quite the challenge as everything needs to be direct value types. My current attempt is with FunctionPointer’s however these need to be static and for that reason i would have to pretty much copy paste every possible combination as a static class / function since im not able to pass per instance data to them (the job itself knows nothing about that so he cant pass that data to it)
are improvements to burst planed that make something like this a easier? what alternatives do i have?
Note: i’m only using production ready packages as of now meaning burst & jobs without entities
i’m still reading but it seems like this is about scheduling multiple jobs for each task. i can’t do that in my case because the job is testing tiles around (a star) dynamically and needs a value right away to proceed and when i schedule the job i have no idea beforehand what tiles it might encounter and under what conditions
You can use what I call “struct behavior injection”.
You create a struct for each implementation of the IPathCostCalculator.
If different calculator use different data you can populate them in the struct implementing that specific calculator and use the data in the calculator method.
Because each calculator is a struct you can use them in burst.
The hard part here is having all within a single job. Has each struct may not operate on the same data they will not have the same size so you can’t stuff them into an array/map.
So you either have to put them individually and come back to your code everytime you have a new implementation of IPathCostCalculator or make a new job not each implementation of IPathCostCalculator.
One other way to investigate would be to try and see something similar to what is done in the physics package for blob collider.
Because all IPathCostCalculator implements the same interface and operate on it’s own data we don’t really care about the data itself when calling the IPathCostCalculator method. So you could try to trick the compilation to think they are all the same.
that’s pretty much what i need. an interface list. its also discussed there (along with some generic solutions that don’t quite seem to be what i’m looking for)
however it looks to be a pure mess with collection types that are only in the entities package and without any documentation compared to the simple List what you would do in c#
i have to say i really dislike working around all the kinks of burst. i have enough challenges getting my code to work as is but i guess i have to dig into how its done and try and replicate that
I have clearly misunderstood what your goals are. There is not always a one-to-one between normal C# and Burst-compatible C#. Burst is fast because it has its strict requirements.
So lets take a step back. Why do you need an “interface list”?
The end goal is simple. for pathfinding i need each tile type to be able to handle the cost dynamically based on the data available.
normal tiles simply have a cost of 1, some tiles have higher cost and some special tiles calculate the cost based on the data. an example for that would be a tile that damages when walked over. low hp entities will have a higher cost based on a formula (max(10 - currenthp, 1) or something) while others have a different cost based on the entity type to make them avoid it
another thing i want to do the same way is mesh creating, where i have a IMeshBufferProvider that returns either a base tile mesh that gets added to the chunk mesh but they can also have modifiers such as variation, state or rotation etc
and kinda have to do it that way because if i try to bake the data into lookup tables they are either enormous or simple impossible to do
i have no idea how to handle this otherwise and the performance part is only second place in this case with flexibility being first.
since even with my current function pointer test the performance is many, many times better than mono c# (1.8ms for processing 65536 tiles vs over 20+ms for a simpler version)
I see. Your pathfinding explanation makes much more sense now, although I still don’t understand your mesh building use case.
As for your pathfinding use case, there is no easy answer here. That partly has to do with the fact that C# doesn’t offer language features for this that are compatible with Burst’s restrictions. And it partly has to do with the fact that Unity does not have infinite resources. Even with the experimental packages, there isn’t an out-of-the-box solution.
One approach you could take would be to emulate std::vector<std::unique_ptr<>> except with a manual Dispose method. The second approach would be to code-gen switch statements for all your unique types based on enums.
I’d just recommend to use Entities for this case, and split it like so:
IComponentData Main struct that has calculated cost;
IComponentData Modifier that you want to run as pre-determined multiplier;
Then just run a system processing modifier and main cost struct that calculates end cost result before cost application system.
Additional modifiers can be added by implementing data component and logic system, cost application code would remain the same.
System / path cost computation can be triggered by either introducing request entity / tag filter, or by manually calling / setting flag on the system (if you don’t need to run it each frame).
Also, introducing extra complexity on top and insane amount of hacks is not the right way to do this.
Even though Entities may not be “production ready” from Unity team perspective - it is mostly ready.
Its Editor integration / utility that is lacking and API that may change (which in fact changes for any part of the engine at will).
entities is a pain to use right now (even if it can do most of the things its missing so much utility you have to do yourself that you spend more time with that than on your actual problem) and even a bigger pain to integrate with normal MonoBehaviours so i wont use it anytime soon.
i would like to port it in the future ( but until then i use jobs & burst just as threading & performance upgrades for normal MonoBehaviour api’s
in addition i don’t like the idea of having all my data floating around on its own on entities. in my opinion it should be structured by having a clear hierarchy similar what i want to do in my case just without entities but that could be just me not thinking “data oriented”
but on the topic of entities. IComponentData seems to be somewhat similar to my problem with entities having multiple structs implementing that interface connected to a entitiy. however internally its very likely to be handled completely differently to my issue
Except, not really. You can author entities even without conversion pipeline directly from MonoBehaviours.
Only issue is that you shouldn’t stall ECS update to keep perfomance high (and jobs running without interruption).
With custom update manager - just update after ECS and you’re good.
I’ve been using hybrid approach for a couple projects for a while and its actually working out great. (boosting performance quite nicely)
Entities support attaching MonoBehaviours directly, and you can even query by MonoBehaviours (which are ComponentDataObjects).
If you want, I can upload github utility classes, in case if you’re looking for non-conversion pipeline entity authoring, its nothing complicated.
And you’ll loose all perfomance benefits. Forget hierarchies, its too complex for it to be really fast.
Unless its tight-loop data, you’ll end up using a bunch of structs in the end anyways.
Because hierarchy is just an order of systems you want data to process in.
1: Don’t use entities for A*. They are not the right tool.
2: Forget about polymorphism, use data composition, and use a functional programming paradigm. Think about your data in terms of data and data structures, not objects.
I can walk you through this very slowly. But please keep in mind that the technique I will be walking you through may have very slow non-Bursted generation of the tile map. The technique will also need to be reimplemented for each interface (just the interface, not each implementer).
Your tile interface methods need to return function pointers to the actual implementations. Don’t worry about what all happens in those implementations yet, just make sure this fits into your architecture. If these methods can be invoked from Burst, then generation of the tile map may also become Burst-compatible.
Define a struct called UntypedInstanceData. Leave it empty for now, but make sure an instance of it is passed into your function pointer delegate.
Check back when you get this far and I will direct you onward to the next steps.
First, recommending Entities to a GameObject-based project is a terrible idea. Entities really wants to control your logical code execution order paradigms in a way that is often too invasive if the project wasn’t designed up-front with Entities in mind.
Second, that technique fails when the cost calculation is dependent on the agent data and there are multiple agents being computed at once.
Aligning ECS update first, then MonoBehaviour update allows to run both in the same project without any issues. I’ve tested this in both medium scale (mobile) and large scale (PC) projects. Note that they were not designed with entities in mind, but rather even before entities package existed.
It can be done in either by using custom update manager, or by modifying player loop. Not sure how that is a terrible idea.
In most of large projects there’s already a custom update manager, due to .Update callback overhead. Implementing one is trivial. Finding one in the net is even more trivial. Refactoring - might not be so much, but in the end you’ll have to do it anyways.
Store entity reference for the entity in a modifier, fetch via CDFE (stuff like hp etc whatever is needed), add up cost as a sum over multiple modifiers, or as a sum + multiplier * initialCost. Different systems would write different modifiers just fine.
It may be not as fast as plain storing everything in native containers and runing processing over them, but its way simpler to implement and maintain. Obviously that doesn’t fit every type of project, but it doesn’t like op has a generic project problem at hand.
In any case I’m not going to argue about this. There’s always a different solution to each problem.
Sometimes simplest one is the right one, sometimes not.
You are very fortunate you have made design decisions that allow this to work for you. Most people run into issues regarding scene management, or issues with linking to presentation and event handling. ECS is a huge learning investment compared to jobs and Burst. And on top of that, it doesn’t receive official support whereas the engine low-level APIs do from what people have told me. Most of the time people are only looking to optimize one part of their game. The last thing they want to hear is that they have to rewrite a major chunk of their game using ECS. This is why this subforum frequently receives posts from people complaining about ECS despite them not having spent much time with it.
I’m not understanding how this solves the problem. Are you proposing running all the systems once for each agent? Otherwise where are you storing those costs? It can’t be on an IComponentData because you could have a thousand agents. Are you using Dynamic Buffers on each tile? That’s a huge memory cost.
thats kinda what i want to do using scriptable objects with a list of interfaces / abstracts that handle data for that type but i cant generalize what they do into just data
i would love to try at least.
kinda have that already packed into an array where tile type id = function pointer array index
easily done
sure thing. im also open for any chatting app for quick discussion sessions if anyone wants
while i would prefer solving it with my current setup i don’t mind looking at pure or hybrid solutions because at best it works great together with my current setup and i can just add it or at worst i at least learned how i would go about it in the future because i do want to use it in the future just maybe not right now for my current project
but i also have to agree with DreamingImLatios. i’m not quite seeing how it would solve some of those issues
either way if you have your hybrid utils somewhere i will definitely take a look
Awesome! I’ll keep the discussion here so that it is public and useful for others.
Time for filling in UntypedInstanceData:
public unsafe struct UntypedInstanceData
{
[Unity.Collections.LowLevel.Unsafe.NativeDisableUnsafePtrRestriction]
internal void* ptr;
public T Resolve<T>() where T: unmanaged
{
T* resolved = (T*)ptr;
return *resolved;
}
[System.Diagnostics.Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
private void CheckNull()
{
if (ptr == null)
throw new System.NullReferenceException("Unable to resolve UntypedInstanceData because its internal data was either zero-sized or not initialized");
}
}
Create a struct that will represent your grid. It should contain a NativeArray which should use ClearMemory when allocated. The struct should also contain an equal-length array of function pointers for each method in the interface. These function pointer arrays should be default-initialized to a function that does not require instance data.
Check back in again when you reach this point, and if you find yourself waiting for me to give the next step, start making implementations that invoke the Resolve method when needed.