API usability

Hi all!

Various suggestions for enhancing API usability have come up in a few different public channels and personal conversations, so we thought it would be a good idea to talk a little more about our intent in a centralized place.

It is no secret that jumping into all of the new high-performance APIs can be challenging! There is a lot of new information to learn, and users are still responsible for managing a lot of complexity and boilerplate code themselves, even in simple use cases. While many factors contribute to how easy it is to learn and use things like ECS, API usability is a major one.

To help focus our efforts on this front, we’ve tried to come up with a more structured approach to evaluating our different usability affordances, so we can identify which ones merit continuation/development, which areas of the API are lacking, and so on. I won’t go into full detail here, but it includes things like

  • How much information developers need to keep in mind for a given task
  • How well it conforms to idiomatic C#
  • How quickly developers get feedback on the correctness of their code
  • How it can mitigate integration discontinuity as developers need more manual control

Applying our usability criteria internally has already helped us identify some areas to clean up, as well as proposals that weren’t helping us move toward our goal of making it easier to write correct, high-performance code than it is to write non-performant code. We’ll use this thread to try to keep you up-to-date about our plans.

Ref Returns

It has been suggested in some places that ref returns could be used to help minimize typing.

// today you need to do something like this
var position = Positions[index];
position.Value.y = someValue;
Positions[index] = position;

// ref returns would enable something like this
Positions[index].Value.y = someValue;

We have currently decided against pursuing this affordance for a few of reasons, such as:

  • It would not be possible to do ref returns with elements in an SOA array, which introduces more inconsistencies in language feature support within a single codebase, and would increase the refactoring burden when migrating to NativeArraySOA
  • It provides little advantage over the status quo (e.g., it is fairly idiomatic C#, and developers currently have to do copy/modify/set of structs in MonoBehaviour C# code; the change would only save 2 lines of typing per struct type)
  • We would have to statically analyze for safe usage, which is doable, but there are other more impactful places for us to invest in enhancements right now

Instead, our goal is to make more code look like IJobProcessComponentData, which bypasses the annoyance introduced by indexing.

8 Likes

In my recent thread Joachim has mentioned you will provide an IJobProcessComponentData that exposes a four component version. Personally I happen to need more (*) on certain occasions and thus have to fallback to the IJobParallelFor and pass multiple ComponentDataArrays of components to the respective jobs which I dislike a lot.

In general the way IJobProcessComponentData is designed with the signature of Execute with all components of a single entity being passed is much cleaner IMHO if you don’t need the index at all.

Is there any information on your stance regarding this scenario? Most notably any explanation whether you will consider adding more component versions akin to System.Action?

(*) More in the sense of two or three components that hold their own data while having multiple other components as “marker components”. An example would be the following set of components on one AI controlled entity where I run a different job per state of entities (a job for all idle grunts, one for all fleeing ones etc.):

  • AgentController
  • AgentGrunt
  • AgentGruntIdleState
  • Animation
  • Position
  • Rotation
  • Scale

Thats great example, when working with Position & Rotation & Scale at the same time. You burn through the 4 component limit pretty quick…

2 Likes

It would be interesting if you could do something similar to a component group

IJobProcessComponentData <Group>
struct Group{
  DifferentComponentDataTypes Data
}
1 Like

It would be nice to be able to group ComponentDataFromEntity just like ComponentDataArray.

The most iteration time I took is the errors related to EntityCommandBuffer. (Mostly duplicated Add, Remove when it does not have one, etc.) The difficulty is that the log does not capture where in the code the command being played back causing the error came from. This means I must not forget to commit more regularly than usual to reduce the possible problem space (by viewing changed files in SourceTree), or else I would have to search for the entire project’s ECB usage.

Currently a solution that dramatically cuts down my debugging time is to go into EntityCommandBuffer source code and add some log what is the type of command being play back right now, to which Entity, and adding what. It floods the log with thousand entries and slow down the game to almost unplayable but at least I have some clue where is the target entity. (With help from Entity Debugger) Still, this way I don’t have the exact originating point of the command.

One another thing which I heard you are already working on is the static analyzer, so the error is revealed before we have to go into play mode. (Which take considerable time to get the game to the point that will trigger that job and reveal the error) I had hit several work rollbacks because I have some job design in my head, spend hours writing it, and I made mistakes like aliasing, something cannot be parallel, etc. and it invalidates my initial design.

We agree IJobProcessComponentData is much cleaner for all the cases it can currently solve. We haven’t settled on specifics yet, but our goal is to improve it to solve more cases and/or build out future scaffolds in similar ways.

Our current inclination is actually to explore alternatives to ComponentGroup structs and move away from them, rather than build out new features for them, but these are useful suggestions so consider them noted :slight_smile:

@5argon Thanks for all these feedback items! We discussed your first two points during our team meeting today (e.g., whether redundant removals should even be an error, how we can improve diagnostics for ECB). As for your question regarding static analysis, our goal is indeed to improve on this front over time. From the standpoint of the usability criteria we are applying now, we are going to strive to identify and inform users of potential problems prior to run-time, in as many cases as it is possible to do so.

3 Likes

That being said, after a quick glance at the chunk iteration archetype, it would be interesting if we could just build a job from that query rather than having to do a ParallelFor. Something similar to the IJobProcess

In terms of api usability, I find math.select() counter-intuitive. The C# ternary operator uses the first expression if true otherwise the second. On the other hand, math select returns the second expression (or value) if true.

Given the lack of proper intellisense documentation, I’d prefer if the two would behave the same way.

2 Likes

I like to think that math.select is analogous to math.lerp as you basically just lerp with 0(false) or 1(true) so it’s more important that those two behave the same way as the operator syntax is very different anyway.

Providing more options to the developers is the way to go.

Make ref returns available, and warn people about the cache misses. Whether or not people use them should be up to them, not the framework designers.

3 Likes

you can use out instead ref, can you not?
I like implemented ECS restrictions, less error prone, which allow closer to unify scripts across the globe.

1 Like

My two cents: I prefer when an API maps to the problems I’m trying to solve in an obvious way. For my experience with the ECS framework so far, most of the problems I want it to solve with my systems have these dimensions:

  • Component dependencies
  • N groups of entities
  • Data dependence/independence between groups

From this perspective, IJobProcessComponentData<> has a really nice interface for declaring limited component dependencies for 1 group of entities operated on independently of other groups.

I haven’t yet seen any nice patterns for processing N>1 groups, nor for handling data dependence between groups.

I think that particularly as performance is such a critical constraint, and given that games are very complicated and frequently need to process interactions between disparate groups of entities, that nailing the API for N>1 dependent groups will be essential for ECS to deliver on its promise of “performance by default”.

For me, this means making it simple and clear, and not having dozens of possible ways to solve the same problem (with highly variable performance characteristics).

As @Antypodish mentioned above, a limited API that addresses the above dimensions would make it much easier to reason about how to solve our game problems, and would result in consistent tutorials, examples, and stack overflow answers, which would lower the learning curve and accelerate uptake.

5 Likes

I’ve got quite a lot of thoughts about this that I’m planning on (have started) writing up which I’ll try to dump here soon.

Will the Burst compiler always run on ECS code, such that an API design that has performance (boxing) issues in standard C# but that could be optimised by Burst would be acceptable? I suspect the answer is sadly no, but I’d like a clarification on that please.

What makes you think no? Can you clarify? Chances are that I misunderstood your question.

Additionally, is it acceptable for the API to have a dependency on C# 7.2? I know there’s talk of moving of moving to C# 7.

Well currently the Burst compiler can (it seems) be enabled and disabled. With the API I have in mind, performance would be horrible without Burst enabled, because there’d be a high rate of boxing of value types. Is that going to cease to be optional at some point? Additionally I’d assume that ECS code may sometimes be distributed in a DLL, and I’m unclear on whether Burst can process a DLL to further optimise.

Basically it wouldn’t have performance-by-default. It would only have performance-after-Burst.

Number of ECS libraries are already wrapped with Assembly Definition files, reassembly dlls.
I don’t that would be a problem, as long code is not obfuscated (or maybe event then?). But don’t know, how burst works behind a scene.

I don’t see reason really for forcing burst to be enabled. It should be optional, if someone want to disable it. Also, you need explicetely declare, which jobs should run on burst using [Burst]. I assume there is reason behind, why is not by default. But this way, it keeps ensure, that if burst is for some reason not compatible with your system / job, you can simply do not activate burst. Which is convenient.

I think working without burst is good starting point for optimization. Then when happy with optimized scrips, aim for enabling burst.on a job(s). That is my opinion.

Also performance is relative term. Still is likely supersede OOP by far, for most, if not all case. Even in bad optimized code, with tons of boxing.

But best, if someone with better expertise, could clarify upon.

I think this option comes with next release of Unity 2018.3
Weather it will be executed straight away, I don’t know.

Perhaps I’m misusing the term Burst if it’s specific to jobs only, I was thinking of it as being a more general optimised compiler for all Unity code that simply currently focuses on jobs.

I’m talking about potentially making every component-read operation boxing. That is definitely not okay. Boxing operations create garbage. There’s a relatively simple inlining that could be done to fix that, but the compiler doesn’t do that on its own and it would need to be done as an additional optimisation in a 100% reliable manner, otherwise you’d have potential GC hell.

It’s not worth going too far into the details of that now, I’ll post further if/when I get my prototype into a decent state. If a Unity dev could comment on the feasibility of that kind of compile optimisation that would be great though.

ECS piratically does not produce GC, if executed correctly. This is one of its advantage. At least that its aim.

And burst yes, it apparently works outside jobs or ECS. But don’t know how effective is there.