Hey #screenshotsaturday! A bit of a different one today, as people have asked me how SOLAS 128 simulates all the pulses and interactions across the entire game without slowing to a crawl.

So I'm going to do a deep dive into how this works! Ready?

1/?

#indiedev #madewithunity
So, let's get you up to speed!

SOLAS 128 plays on a 253x253 grid (64,009 spaces), and has approximately 18,500 different elements a pulse can interact with. By the end of the game we need to calculate 1,600+ pulses' interactions... in less that a single frame.

Yeah...

2/?
The first optimisation is that the game works solely on the grid. Each square can only have a single pulse, and a pulse only exists 'as rest'. The underlying simulation therefore needs to figure out for each pulse what space it should be in at the next 'tick', plus its colour 3/?
That explains diagonals - pulses can't exist 'between' grid spaces. Each tick every pulse needs to calculate:

1. Its new grid location
2. Did it hit a piece?
3. Did it hit a pulse?
4. Did it hit a filter?
5. Did it hit a prism?

These can all change its direction or colour!

4/?
How do we do this fast? Well, it's all down to storage. Each pulse is a single 'byte'. That's 8 numbers which can be 0 or 1. That's it! We then have 64,009 bytes (64 kilobytes) that defines all the pulses that exist!

Each number defines one element on or off...

5/?
This works really well for us, as bytes can be processed VERY quickly. It's what computers are good at! Also, it works well with colour as we can define all of the ones we use with only 3 bits, the primary colours.

Want to make yellow? Well, that's just red and green...

6/?
It works with movement on a grid too. What is a SE diagonal? It's just move right one and down one...

So, *everything* in SOLAS 128 uses this. Pulses, emitters, filters... The really nice part is it works with the additive glyphs too. We just turn on the bits which are 1s!

7/?
But... how do we know which bit is on, or do things like merging pulses? That all lives in truth/logic tables that any coder will vaguely remember.

Say a red & green pulse collide, to the code that looks like:

001 | 010

Well, we can bitwise OR these together to get 011

8/?
That sounds complicated but really just means the computer lines up each byte like this and compares them:

00000001 - red
00000010 - green

For each column (bit) where either row has a 1, the result also has a one:

00000011 - yellow

This is a VERY fast operation too 🥳

9/?
Look how powerful this is! For any collision we can now just OR two pulses together and figure out the new direction and colour in one fast operation!

Unfortunately... it's never quite *that* easy, due to cases we need to make behave more naturally, but it's the basics 💜

10/?
This technique, by the way, is called 'bitmasking' and has been around for ages. I remember being confused when Quake 2 modding how enemies could store what difficulty they'd spawn in within a single number. It was because it was using individual bits just like this!

11/?
Anyway, my favourite part of this is how we can use different logical operators for different features.

If we use NOT, then we flip all the bits: 010 -> 101

If we AND rather than OR, Then the result will only be 1 if both bits are 1: 011 & 110 -> 010

That brings us to...

12/?
Filters! They need to remove colours, right? Well, the colour of the filter is stored as a byte too. So, lets use what we just learnt

00000100 - blue filter
¬
00000011 - (NOT blue)
&
00000111 - white pulse

00000011 - yellow pulse

We've removed blue and it all just works!

13/?
The really nice thing here is what happens when it gets more complicated?

00000101 - magenta filter
¬
00000010 - (NOT magenta)
&
00000001 - red pulse

00000000 - no pulse

A magenta filter will kill a red pulse by default. Perfect! White filters therefore stop all pulses!

14/?
And remember, these operations are amongst the fastest a computer can do, so it allows us to do these for all our pulses incredibly quickly! We can merge/filter/split them all using similar techniques of varying complexity which makes my coding side a very happy boy 💜

15/?
There is one more aspect, however. If we checked all 64,009 spaces this would still be too slow... so the final major optimisation is that we keep a list of all the 'alive' pulses, and only process those. Simple, right?

Well... mostly. How do we tell when a pulse dies?

16/?
Initially this seems easy! If the pulse is 00000000 then it's dead, and we can remove it. But, wait... a pulse that has had all its colour removed will still have a direction, but needs to be marked as dead. Same if it has colour, but no direction. We need to be cleverer!

17/?
So, back to some logic & bitmasks. We know we AND two bytes together, and how that works... so, if we define a mask for the direction then AND with that, we can pull out JUST the direction component:

01111000 - direction
&
00000010 - pulse

00000000 - dead pulse

Oh look...
18/?
Great, so now we can check if a pulse has lost all of it's direction or all of it's colour and mark it as dead. Perfect!

So, if the pulse is dead, we remove it from our list of alive pulses and don't do any further processing with it!

Thanks bitmasking 💜

19/?
This, by the way, is what leads to the weird looking if statements you might have seen in previous tweets. As if we want to extract whether a single bit is on, we AND with a mask of just that bit, then test to make sure the end value isn't zero.

Yeah, it hurts my head too!

20/?
So, #screenshotsaturday that's how SOLAS 128 can process 1,600+ complex pulses in less than 16ms. A bunch of optimisations mostly involving bytes and boolean logic!

I've skipped over so many corner cases here (prisms are a nightmare) but those really are the basics!

21/?
If this was interesting let me know! SOLAS 128 really is a bunch of smoke and mirrors (teehee) and there is so much to talk about. Filters, for example, are a spatially linked list, which is wild 🤯

Oh and buying/reviewing it is a huge help: armor.ag/SOLAS 💜

22/22
As people are enjoying this I've got a little addendum to how the pulses work! I said earlier that I use 64,009 bytes to store all the pulses, but that's not strictly true. To make collisions work, pulses all need to be processed in parallel, but that's not possible, so...

23/22
...why is that? Well, what happens if I have a red pulse moving to the right, and a green pulse moving to the left, one square from each other? Whichever one I process first would overwrite the other one unless I move them at the same time. One square, one pulse, remember?

24/22
But I process them one at a time, so what do I do? I use a trick from cellular automata: I have a 'current' pulse state, and a 'new' pulse state. I read from the current, write to the new. When I'm done, I empty the input so I can use it as the output next tick. Flip flop

25/22
This works great! No accidental overwriting, all the pulses merge correctly, it allows us to deal with issues where more than two pulses collide in the same space. Flip flop flip flop.

In fact there is only one real downside, that is we need double the storage space...

26/22
That's right, instead of needing 64KB of memory to store and process all the pulses, we now need... you guessed it: 128.

Now why does that number sound so familiar? 🤔

Shh, don't tell anyone, yeah? If you made it this far it can be our little secret 💜

27/22
Anyway, if you've got any questions about how any of this works, do feel free to drop me a question and I'll see if I can answer it!

I'm honestly pretty rubbish at optimisation in general but I'm dead proud of what I managed to get working in SOLAS 128 🥳

28/22
And as always, buying/reviewing/talking about games from small teams (3 people, I'm the only dev) is hugely appreciated and means we can continue making cool stuff!

Steam: armor.ag/SOLAS
Switch: armor.ag/SOLAS128-Switch

#indiedev #IndieWorldOrder #madewithunity

29/22

• • •

Missing some Tweet in this thread? You can try to force a refresh
 

Keep Current with Amicable Animal - SOLAS 128 is out NOW!

Amicable Animal - SOLAS 128 is out NOW! Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

PDF

Twitter may remove this content at anytime! Save it as PDF for later use!

Try unrolling a thread yourself!

how to unroll video
  1. Follow @ThreadReaderApp to mention us!

  2. From a Twitter thread mention us with a keyword "unroll"
@threadreaderapp unroll

Practice here first or read more on our help page!

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3/month or $30/year) and get exclusive features!

Become Premium

Too expensive? Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal Become our Patreon

Thank you for your support!

Follow Us on Twitter!