#So Much BoilerPlate 😖 Please Help

1 messages · Page 1 of 1 (latest)

chrome dome
#

I have a simple culling system that needs to add DisableRendering to Entities that fall out of range, and remove it from Entities coming into range.

I think the boilerplate-to-code ratio exceeds 10 lines to 1 line. This is untenable. What other patterns or suggestions to you have? (this is not about optimization of the distance check yet, but I would like to optimize the structural change to happen only for those where it is needed.)

Complications: SOME of the entities have an associated UIEntity, but not all.

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var cameraState = SystemAPI.GetSingleton<CameraState>();
            var ecb = GetEntityCommandBuffer(ref state).AsParallelWriter();

            var show = new ShowEntitiesInCameraRange
            {
                _ecb = ecb,
                cameraPosition = cameraState.position
            };
            show.ScheduleParallel();

            var show2 = new ShowPipsInCameraRange
            {
                _ecb = ecb,
                cameraPosition = cameraState.position
            };
            show2.ScheduleParallel();

            var hide = new HideEntitiesOutsideCameraRange
            {
                _ecb = ecb,
                cameraPosition = cameraState.position
            };
            hide.ScheduleParallel();

            var hide2 = new HidePipsOutsideCameraRange
            {
                _ecb = ecb,
                cameraPosition = cameraState.position
            };
            hide2.ScheduleParallel();
        }
#
        [BurstCompile]
        [StructLayout(LayoutKind.Auto)]
        [WithAll(typeof(LocalToWorld), typeof(DisableRendering))]
        [WithNone(typeof(TagSpace))]
        internal partial struct ShowEntitiesInCameraRange : IJobEntity
        {
            [ReadOnly] internal double3 cameraPosition;
            internal EntityCommandBuffer.ParallelWriter _ecb;

            [NativeSetThreadIndex] private int _thread;

            [BurstCompile]
            [UsedImplicitly]
            private void Execute(Entity entity, ref Position position)
            {
                if (math.distancesq(position.value, cameraPosition) <= Constants.CullRange * Constants.CullRange)
                {
                    _ecb.RemoveComponent<DisableRendering>(_thread, entity);
                }
            }
        }

        [BurstCompile]
        [StructLayout(LayoutKind.Auto)]
        [WithAll(typeof(LocalToWorld), typeof(DisableRendering))]
        [WithNone(typeof(TagSpace))]
        internal partial struct ShowPipsInCameraRange : IJobEntity
        {
            [ReadOnly] internal double3 cameraPosition;
            internal EntityCommandBuffer.ParallelWriter _ecb;

            [NativeSetThreadIndex] private int _thread;

            [BurstCompile]
            [UsedImplicitly]
            private void Execute(UIEntity ui_entity, ref Position position)
            {
                if (math.distancesq(position.value, cameraPosition) <= Constants.CullRange * Constants.CullRange)
                {
                    _ecb.RemoveComponent<DisableRendering>(_thread, ui_entity.value);
                }
            }
        }

        [BurstCompile]
        [StructLayout(LayoutKind.Auto)]
        [WithAll(typeof(LocalToWorld))]
        [WithNone(typeof(DisableRendering), typeof(TagSpace))]
        internal partial struct HideEntitiesOutsideCameraRange : IJobEntity
        {
            internal double3 cameraPosition;
            internal EntityCommandBuffer.ParallelWriter _ecb;

            [NativeSetThreadIndex] private int _thread;

            [BurstCompile]
            [UsedImplicitly]
            private void Execute(Entity entity, in Position position)
            {
                if (math.distancesq(position.value, cameraPosition) > Constants.CullRange * Constants.CullRange)
                {
                    _ecb.AddComponent<DisableRendering>(_thread, entity);
                }
            }
        }

        [BurstCompile]
        [StructLayout(LayoutKind.Auto)]
        [WithAll(typeof(LocalToWorld))]
        [WithNone(typeof(DisableRendering), typeof(TagSpace))]
        internal partial struct HidePipsOutsideCameraRange : IJobEntity
        {
            internal double3 cameraPosition;
            internal EntityCommandBuffer.ParallelWriter _ecb;

            [NativeSetThreadIndex] private int _thread;

            [BurstCompile]
            [UsedImplicitly]
            private void Execute(in UIEntity ui_entity, in Position position)
            {
                if (math.distancesq(position.value, cameraPosition) > Constants.CullRange * Constants.CullRange)
                {
                    _ecb.AddComponent<DisableRendering>(_thread, ui_entity.value);
                }
            }
        } 
#

How would you do this better? I might remove a few extraneous attributes, but the amount of boilerplate to do something so simple is absolutely stunning.

#

Holy shit, the actual ratio is 40 lines of boilerplate to 1 line of feature code (160:4) when looking at the whole ISystem.

fair spruce
#

You could just write one IJobChunk to reduce amount of jobs to one

#

[UsedImplicitly] also you don't need those at all

#

as well as [StructLayout(LayoutKind.Auto)]

chrome dome
#

Yeah I can get rid of about 12 lines of unneeded attributes.

#

I don't quite understand how IJobChunk helps here.

fair spruce
chrome dome
#

Ah, yeah.

#

That's pretty much what I would have needed. I'll try that!

fair spruce
#

allthough, that might not reduce amount of lines in total

#

wait, why do you have jobs doubled?

chrome dome
#

They aren't doubled. Half read the UIEntity component and enable/disable rendering on that referenced entity, the other half does it for the entities themselves.
"Position" is a special double precision position, I think I don't want that on the UI entities if I can help it. (might actually add it, though - but then copy jobs need to move significantly more data to keep them in sync, and not all UI entities actually live in "open space")

#

Not all entities have a UIEntity component.

#

One approach could be to dictate "nope, you all need a UIEntity".

#

That could work.

#

I could put "Entity.null" in there and test for that.

#

It would add about 1 or 2 lines of extra code to a few extra jobs, but these "duplicate" jobs would evaporate into nothingness.

limber marlin
#

Your style of coding is alone adding 5 unnecessary lines.

why do you need to store the job in a local field before scheduling, you never reuse that variable elsewhere

chrome dome
#

That stuff is from scheduling work, I plan to schedule these jobs in a bundle and combine dependencies. There is a job afterwards that I removed.

#

I did not count it and its code in this ratio. It's actually better for that job, it at least has more than 1 action. 🙂

limber marlin
#

Many of my systems are 1 line of code in on update

chrome dome
#

I wonder if I could write something like a :

struct GenericRemoveComponentsJob<TICD,TICD,TICD,TICD, Comoponent>(Predicate)

or something.

#

(super crudely typed up, need to think how that could actually work if at all)

limber marlin
#

Ah the classic software engineering solution.
Spend more time write to less code than you'll saving just writing the code 🙃

chrome dome
#

Nah.

#

This amount of boilerplate is way past acceptable.

#

I'd rather write Hello World in Java for the rest of my life than this.

#

Just the refactoring effort alone is crushing with these 4 jobs. (this is how I got here)

#

Sometimes the attributes encode the behaviour, sometimes the Execute parameters, sometimes both (redundantly, sometimes not), etc.

limber marlin
#

If I wrote this my in update would be 4 lines + a query which I can't determine from your code

#

2 lines to cache values, lines to schedule 2 jobs

#

That's it

chrome dome
#

Cache what values?

#

There's nothing cacheable.

limber marlin
#

Ecb and camera position like you do

chrome dome
#

Ah k.

#

That's "passing" for me.

#

But no problem.

#

I could write it with a lookup, or, as earlier suggested, with an "empty" component (that actually turned out to be a complete headache, because it would force an extra component into ~10 unrelated archetypes - so currently I stick with 4 jobs).

#

I wrote some jobs that have an "enable" or "disable" flag I can pass in, but I can't do that when the archetype isn't the same (then I just have more code in query builders, and also there's the problem that apparently queries aren't evaluated on schedule, but on run - then suddenly, it becomes a concurrency problem; not exactly in this case, but there are some others)

limber marlin
#

hthere's so much uncessary stuff here

#

[StructLayout(LayoutKind.Auto)]
[NativeSetThreadIndex] private int _thread;
[UsedImplicitly]

chrome dome
#

Yeah, that's the point of this thread.

limber marlin
#

should all not be added

limber marlin
chrome dome
#

But I need that.

#

Can't write to ParallelWriter without.

limber marlin
#

no you should use chunk index

#

passed in execute

chrome dome
#

huh?

limber marlin
#

Execute([ChunkIndexInQuery] int chunkIndex

chrome dome
#

Ok saves a line and some characters.

#

Classic Unity long type name again.

#

It's still the same "amount" of boilerplate, just moved into an already crowded line.

limber marlin
#

well it's not because it's removed a new line as well ^^

chrome dome
#

So now I'm at 130 lines to 4 lines.

limber marlin
chrome dome
#

If I remove all "necessary" stuff, it would be ~100:4, still atrocious.

limber marlin
#

i don't know man, i feel like you've shot yourself in the foot and are complaining about the person who gave you a gun

chrome dome
#

The job name needs a clear name.

limber marlin
#

and so do attributes

chrome dome
#

No, [ChunkIndex] would be enough.

#

Or [Index]

limber marlin
#

the difference is, you're the only person who needs to know what the job does

chrome dome
#

Or, just accept an int.

limber marlin
#

everyone needs to know what the attribute does

chrome dome
#

Like they also accept an Entity parameter.

limber marlin
#

there are multiple different ints you can pass in via execute

chrome dome
#

Such as?

#

This is interesting!

limber marlin
#

[EntityIndexInQuery]

#

[EntityIndexInChunk]

#

[ChunkIndexInQuery]

chrome dome
#

hmm.

#

I wouldn't even know what to use the first two for, other than some averaging calculations, but cool. Thanks.

#

Thank you for your input.

limber marlin
#

my styling would have it like this

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var cameraState = SystemAPI.GetSingleton<CameraState>();
            var ecb = GetEntityCommandBuffer(ref state).AsParallelWriter();

            new ShowEntitiesInCameraRange { _ecb = ecb, cameraPosition = cameraState.position }.ScheduleParallel();
            new ShowPipsInCameraRange { _ecb = ecb, cameraPosition = cameraState.position }.ScheduleParallel();
            new HideEntitiesOutsideCameraRange { _ecb = ecb, cameraPosition = cameraState.position }.ScheduleParallel();
            new HidePipsOutsideCameraRange { _ecb = ecb, cameraPosition = cameraState.position }.ScheduleParallel();
        }```
10 lines of code instead of 57
#

as long as the line is less than 160 characters, i'm happy with it on 1 line

#

(well i'd never have 2 jobs for show/hide)

chrome dome
#

Yeah that works if you don't have a follow up job that depends on the others. Or you want to run them all in parallel.

#

Still same amount of statements.

#

== boilerplate.

limber marlin
#

each line does 1 piece of work

chrome dome
#

Like I said in OP, I actually count this as 10:1, the 40:1 was the raw score. 10:1 is awful, Java HelloWorld is around 7:1 and is generally ridiculed.
It's pretty atrocious. Sadly, I've failed at writing any sorts of generic jobs that can do two or three things, or the same thing to a different query. (well, then the main problem is writing that query, which adds bloat again).

limber marlin
#

c# hello world is 8-10 lines

chrome dome
#

No, C# hello world has been 1 line.

#

For the past 3 years or so.

limber marlin
gaunt raven
limber marlin
#
namespace HelloWorld
{
    class Hello 
    {         
        static void Main(string[] args)
        {
            System.Console.WriteLine("Hello World!");
        }
    }
}```
chrome dome
#

Yeah I don't want super short code, but 10:1 is pretty bad.

#
Console.WriteLine("Hello, World!");
gaunt raven
#

You have to also accept that it is doing things you are asking for

limber marlin
gaunt raven
#

You have chosen jobs, parallelism, etc

chrome dome
#

Anyway. I'm looking for ways to cut down on the huge amounts of BP.
I was able to get a bunch out by removing extraneous attributes.

#

Whitespace doesn't really count as BP.

#

But ... parameters, attributes, functions, etc. do.

#

As does every statement that doesn't actaully do the work.

#

3:1 ? would be stellar.
5:1 ? begrudgingly
10:1 ? how about no.
20:1 to 40:1 ? I had to count twice to believe it.

gaunt raven
#

@limber marlin Kieren showed me that Big System you made at work that has like a million lines to gather component lookups and other shit and that's all I can think of right now hahaha

limber marlin
#

haha the we merged 16 ai systems in 1

#

because console performance was busted?

gaunt raven
#

That's the one lol

limber marlin
#

it did reduce cost from 6ms to like 0.5

#

this is why isystem is important!

#

the manual burst compiling of getters

#

in theory the guy that wrote the AI system did a great job

#

in practice it turned out to be all sorts of problematic

#

i always wondered how it'd do in 1.0 in comparison

#

if it could now handle all those small systems etc

gaunt raven
# chrome dome 3:1 ? would be stellar. 5:1 ? begrudgingly 10:1 ? how about no. 20:1 to 40:1 ? I...

We all agree that this is a lot of code, but I think it's worthy to note that what you're being enabled to do is not simple.
I think you could write a source generator if you found this exact thing was a problem often, but I don't think your code necessarily will look like this often, and so I'm unsure whether Unity can cater for it as well as you yourself could.
They have repeatedly stated they're aware of the problem, and things have slowly improved and changed over the years, some of it for the worse as they discover problems like the sourcegen slowdown they had with EFE

limber marlin
#

i'm kind of curious what your proposed solution to this is

#

like, what could you remove or change to make it perform the same while still keeping hte same behaviour?

#

maybe auto injected ecb?

gaunt raven
#

Presumably a SystemAPI query like thing, but for parallel jobs

limber marlin
#

well see i personally hate that kind of thing for any type of actual logic

#

i find it terrible for code maitanance

#

if your job is so small that it could work in SystemAPI.Query then it probably means it shouldn't be a parallel job in the first place

#

anyway, obviously less boilerplate is better if it can be removed without impacting readability

#

systemapi was amazing for this and hopefully we get further improvements

#

that said, people complain way too much about this

#

it really doesn't matter to your productivity

#

and the only thing worse than verbose code is really concise code that no one else can understand...

gaunt raven
#

I agree generally. I do find it time consuming to refactor these large structures when they start at a higher level and you need to walk them towards the chunk level to bypass something that just doesn't seem to work at the level you might have written it

#

This will all get better with time, after the latest Dev day it sounds like there's important fixes coming that will prevent many times I've been forced to do that

limber marlin
#

You know the 1 tool i'd love

#

IJE -> IJC clean conversion

#

i keep writing ije then hating myself and converting it to ijc

#

but the code gen it produces is not exactly user friendly and takes more effort to cleanup than to write it again by hand ^_^'

gaunt raven
#

One day I'll learn how to make a rider plugin

#

Alt-enter my pain away

limber marlin
#

hmm i'd pay for this