Fork me on GitHub
#clojure-gamedev
<
2016-02-25
>
jcromartie01:02:25

so, my totally naive entity system has 1000 entities with position and velocity components, with all of those components being updated in one tick, and it can do that 1000 times per second

jcromartie01:02:51

there's no optimization there at all

jcromartie01:02:03

but 1000 is not a lot

solicode01:02:48

Hmm, and that’s per second… even per frame that’s not a lot.

solicode01:02:56

Just curious, is this Clojure or ClojureScript?

solicode01:02:00

Not that it matters much

jcromartie01:02:10

it might be too clever

jcromartie01:02:11

too many maps

solicode01:02:48

I was going to attempt this with ClojureScript (just a simple game using ECS), and I have a feeling I'm going to run into the same issues you're having. I'm curious where the bottleneck is.

solicode01:02:55

There’s this, but I haven’t looked at the code yet: https://github.com/markmandel/brute

jcromartie01:02:58

the basic design is a hash-map where the keys are component names and values are maps of entity ids to component values

jcromartie01:02:30

yeah that looks familiar

markmandel01:02:37

Hey, that's mine 😎

jcromartie01:02:37

that's basically what I'm doing

markmandel01:02:44

Let me know if there is anything you'd like to see in brute

jcromartie01:02:46

is that (transient system) really doing anything in add-entity?

jcromartie01:02:53

since it's just one assoc?

jcromartie01:02:11

I would imagine you'd want to batch a lot of assoc! calls together

markmandel01:02:28

Hmnn... I wrote that a while ago, have to take a peek

markmandel01:02:24

I hit system twice in that function

jcromartie01:02:25

but great minds think alike simple_smile

jcromartie01:02:28

this is exactly what I'm doing

jcromartie01:02:52

twice in add-component but once in add-entity

markmandel01:02:14

There an Assoc and a retrieval

markmandel01:02:45

There's two threading macros there

jcromartie01:02:32

does transient affect get too?

markmandel01:02:48

I have to look at it, but I wonder if I can just make it an update-in

jcromartie01:02:43

would have to benchmark the difference

solicode02:02:06

@markmandel This is neat. I'm curious what the performance is like. I'll see if I can give it a try this weekend.

markmandel02:02:07

Be easier to read at least

solicode02:02:12

And if there’s room for optimizations.

markmandel02:02:36

I'm writing a top down racer with it, but it's not exceptionally complicated (yet?)

markmandel02:02:06

Update-in would be far more readable though

markmandel02:02:22

On my phone atm , can't look 🙁

solicode02:02:46

I really don't know much about Specter or if this would even be applicable, but its precompilation feature looks interesting: http://nathanmarz.com/blog/functional-navigational-programming-in-clojurescript-with-sp.html

solicode02:02:10

Seems to yield good results

solicode02:02:19

the benchmarks are at the bottom

jcromartie02:02:50

Specter is cool

jcromartie02:02:07

OK so, some more relevant numbers

jcromartie02:02:19

I can update 8000 entities 60 times per second

jcromartie02:02:40

that would be really hard to keep in sync with graphics at 60 fps but it could work

jcromartie02:02:09

of course that's a really simple system with just random velocity and position += velocity

jcromartie02:02:19

with the velocity changing every tick

jcromartie02:02:31

just found this

jcromartie02:02:44

commenting on the "problems" with some Clojurescript game demo

jcromartie02:02:34

I don't know how someone got the idea that just iterating over a 100 element list would take 16 ms

jcromartie02:02:54

I can reduce over a 1 million element vector in less than that

jcromartie02:02:06

but this post was a long time ago

jcromartie02:02:09

almost 4 years ago

solicode03:02:07

4 years, wow. ClojureScript was around then? But yeah, both ClojureScript and JS engines have improved greatly since then.

hugesandwich03:02:36

I may have some tips for you guys speeding up your iterations and about the entity system structure, but I am kind of beat for the day

hugesandwich03:02:45

one thing we did in C/C++ was to keep a free list

hugesandwich03:02:32

we used tricks where we could index back into an array and keep it nice and packed by mucking about with the ids of entities and such matching array indicies

hugesandwich03:02:45

it's not that hard to do, just a little mind bending at first, but an old old trick

hugesandwich03:02:03

I strongly discourage using hashmaps if you can avoid it..

hugesandwich03:02:11

i suppose for components themselves, it's fine

hugesandwich03:02:31

but when you iterate and pool things, you probably want to be using tightly packed arrays

hugesandwich03:02:00

I wish i knew more about how clojure data structures interact with cache lines, but I suspect not so well

hugesandwich03:02:39

anyway, somewhere there's a GDC or similar conference paper where a guy has good info about most of what I just mentioned

hugesandwich04:02:13

used to be some good info here - http://bitsquid.blogspot.com/2011/09/managing-decoupling-part-4-id-lookup.html. It's about C++, but a lot of it is applicable. Actually I'd pay a lot more attention to anything about C++ rather than Clojure/Java when thinking about entity systems. It's been a bit since I looked, but most of the info is pretty bad and has awful practices like using uuids for entity ids, tons of boxing, iterating per entity instead of per component type, pub-sub, etc. and things like that. 95% of what is out there is misinformation or overly naive and is either just wrong or will kill your performance.

hugesandwich04:02:12

I'd quickly add on the clojure side to avoid sequences and do ugly things like loop/recur rather than some of the other "good clojure" practices if you need performance in your loops

hugesandwich04:02:43

also try to use any bitwise operations when possible. Lots of great tricks here for more than you might think to squeeze some juice out of java/clojure

hugesandwich04:02:50

And unchecked math can help

hugesandwich04:02:15

finally, use pooling as much as possible as we discussed earlier, whether cljs or clj. It really does make a difference. First pool everything, then bench. And be careful of your benching techniques, i.e. don't rely on using (time), cold jvm, etc.

hugesandwich04:02:23

hope that helps some

jcromartie13:02:42

@hugesandwich: I think the basic API I have leaves room for optimizations later; I am using integers for entity ids, and hash maps to store {component-name {entity-id component-value ...} ...}

jcromartie13:02:40

when updating a component map (that is a map of all entity ids to component values for single component, like position or health etc.) I use a transient and only assoc! a new value when it's non-nil and different from the old value, so that for one tick there's only one new hash-map created per component type

jcromartie13:02:32

that avoids iterating per entity, etc.

hugesandwich14:02:49

@jcromartie sounds good, assuming you are doing the diff check efficiently with clojure and not a deep comparison

jcromartie14:02:15

yeah the component values are clojure values so equality is very cheap to check

markmandel18:02:22

So I use UUIDs in brute - but that's probably because of my naivety... it's not been a performance bottleneck for me - but would be interesting to do some benchmarks

hugesandwich19:02:07

the reason to not use UUIDs is not just speed using them/generating them, but to more importantly have the opportunity to use them as a cheat to index back into your pools/freelists for entities

hugesandwich19:02:22

I believe a couple of Java entity systems use UUID

hugesandwich19:02:25

another reason is for better or worse, you can make integers play nicely with tools and pre-define some ids if needed. Obviously things can go very wrong with regard to collisions and such, but your tools and id allocator should be using the same code anyway so in practice it shouldn't be an issue

hugesandwich19:02:44

integers are in the end much lighter weight in every way from memory onwards. That said, you need to write a good id manager that safely allocates across threads if needed, so UUID doesn't have this issue at all

jcromartie20:02:44

I want to see how this system works for a Zelda-style game, I’m not building Skyrim yet simple_smile

jcromartie20:02:57

but yes, those were my thoughts exactly about integers vs guids

jcromartie20:02:54

though in Clojure the standard number is a 64-bit signed Long

jcromartie20:02:05

the standard integer type

jcromartie20:02:14

so when you type a literal 42 you get a long

jcromartie20:02:26

not that you’d be typing entity ids into source code

jcromartie20:02:30

but I am sure I could get away with guids and never see performance issues