English Amiga Board

English Amiga Board (https://eab.abime.net/index.php)
-   Coders. General (https://eab.abime.net/forumdisplay.php?f=37)
-   -   Wrong Way Driver - Tech (https://eab.abime.net/showthread.php?t=106671)

pink^abyss 19 April 2021 15:32

Wrong Way Driver - Tech
 
Some people wanted to hear more about how Wrong Way Driver was created.

Here are some details:

The game is written for OCS A500 + 512kb (tho with some extra work it would fit into 512kb only).

C++20
The game was born out of an experiment if it would be possible to code a fast Amiga 500 game
using C++20 with Coroutines and Lambdas. Bartmans "Amiga C/C++ Compile, Debug & Profile" which i used
has support for them. I guess this is the very first C++20 program written on any Amiga computer :)

Coroutines
Coroutines let you 'pause' or 'yield' your code and continue later at the same point. The context for
them is not stored on the stack but on a heap. Here is a pseudo example how i used them in the title screen
to move the logo, print some text and wait for the fire button in parallel by launching another coroutine.

Code:

coroutineHandler.add([&]() -> coro
{
  //move in the logo
  for(int x=-160;x<160;x++)
  {
      showLogo(x);
      co_yield 0; //wait for next frame
  }

  co_yield 50; //wait 50 frames

  //add another coroutine to wait for fire button in parallel
  coroutineHandler.add([=]() -> coro
  {
      while(1)
      {
          if(fireButtonPressed())
            gGame.setState(Game::State::intro);

          co_yield 0;
      }
  }

  //show blinking text
  while(1)
  {
      auto text=printText("Press Fire!);
      co_yield 20;
      clearText(text);
      co_yield 20;
  }
}

"coroutineHandler" is a container for the coroutines. The coroutineHandler is embedded in the current gameState and 'ticked' from there.
If i change the gameState then the coroutineHandler gets deleted. So i don't have to care about dangling coroutines.

Memory Managment
For this game i used again no dynamic memory managment (so no 'new' and 'delete', but only placement 'new'). I used preallocated Frame- and Poolheaps.
I described such heaps already for Tinyus Tech.

Sprites
Sprites are used for the "WRONG WAY DRIVER" and "ABYSS" logo and all HUD elements like the coin/gas counter and score display.
On the title screen i use 3 color sprites, and when playing the game i use attached 15 color sprites.
A single 128x224 sized area, with two bitplanes, is used for all sprites. A software driver sorts, splits, blits and multiplexes them into this
area each frame as needed. Writing an optimal sprite driver is a madmans task.

The city skyline
The city area uses dual playfield for parallax scrolling. Various copper splits are used to make it look
like more then two playfields. Also sprites are displayed above to make it look like 4 parallax layers are there.

The pseudo 3D street
The street is created with 5 parallaxed stripes. Each uses 16 frames of animation (3kb altogether packed).
They are blitted into the backbuffer and into the restorebuffer each frame.
The different cars, coins and gas symbols are just one image each. On startup i precalc 11 zoom images for each of them.
The street area runs in 4 bitplanes. I used two coppersplits to parallax animate the front and back side of the street.

Bobs
I use 16 and 32 pixel wide bobs for all zooming objects. Each bob can have a variable height.
For optimal performance i blit all of those after waiting for the last displayed line.

Audio
All insturments+fx take 34kb, and are precalced on startup (Pretracker!).
Channel 4 is used mainly for delay sounds and can be interrupted by SoundFx.

Game size
The game was crunched with Shrinkler from 135kb to to 61kb. The large size is mainly because of the used GCC optimizations (lto,-Ofast).
Without GCC optimizations the game is only half as big. By sprinkling #pragmas over non time critical code the size could be further reduced.


Thanks for reading.

Jobbo 19 April 2021 15:46

Very interesting to see co-routines in use on Amiga. I haven't used them myself in other projects but I've seen the concept used in games at least as far back as 1998.

What is the overhead like for yielding and continuing on the Amiga? Is the optimizer able to inline everything to such an extent that it only needs to save a partial context?

pink^abyss 19 April 2021 16:33

Quote:

Originally Posted by Jobbo (Post 1477860)
Very interesting to see co-routines in use on Amiga. I haven't used them myself in other projects but I've seen the concept used in games at least as far back as 1998.

What is the overhead like for yielding and continuing on the Amiga? Is the optimizer able to inline everything to such an extent that it only needs to save a partial context?

Code generation within a coroutine follows the same rules as for any other function call. However, the coroutine itself can't be inlined. A coroutine yield will just restore registers and do a rts.
The code for iterating over and jumping into the coroutines adds also a little bit of extra work, but compared to just calling an array of function pointers and context its not much slower (at least when each of this functions also actually do some work).

Coroutines allow shorter, easier and much better readable code, for very little extra performance impact. Especially for typical 'throw away' game code that only happens once in your game without much structure. :great

jotd 19 April 2021 21:28

Bagman was probably the first amiga game to use C++11, you broke that record with C++ 20. I'm jealous.

DanScott 19 April 2021 22:53

Coroutines are banned where I work :D:D

Invoke too...

Jobbo 19 April 2021 23:15

Please make them add the spaceship operator to that list!

jotd 19 April 2021 23:45

spaceship operator what a joke, this is just useless...

pink^abyss 21 April 2021 16:40

Quote:

Originally Posted by Jobbo (Post 1477860)
Very interesting to see co-routines in use on Amiga. I haven't used them myself in other projects but I've seen the concept used in games at least as far back as 1998.

What is the overhead like for yielding and continuing on the Amiga? Is the optimizer able to inline everything to such an extent that it only needs to save a partial context?

Regarding overhead for continuing a coroutine:
A jumptable call is used as indirection on each resume (if you have more then one co_yield). So the overhead compared to a normal function call is negligible (at least compared with a function that would need to save/restore all registers).

Another feature i didn't talk about was the use of lambdas in Wrong Way Driver. Those imposed no performance penalty and helped a lot to shorten the code and make it more readabale.
Lambdas are great for callbacks or short 'throw away' code snippets that you don't need in a function.

Jobbo 21 April 2021 17:06

It's really interesting to hear these features can work effectively on a 68000.

I'm not sure how I would feel about opening this can of worms on a large project. But since most Amiga projects are tiny teams or just an individual it seems perfectly fine.

Certainly seems like it could simplify a lot of the high level sequencing code for a demo or game.

pink^abyss 21 April 2021 19:50

Quote:

Originally Posted by Jobbo (Post 1478318)
It's really interesting to hear these features can work effectively on a 68000.

I'm not sure how I would feel about opening this can of worms on a large project. But since most Amiga projects are tiny teams or just an individual it seems perfectly fine.

Certainly seems like it could simplify a lot of the high level sequencing code for a demo or game.


I see your point (and i'm certainly not an advocate for modern C++ stuff). I used these things on Amiga 500 because i used them successfully already many years in my real world coding job.There we don't use coroutines in the game engine code, but we use lambdas a lot (we see them just as 'local' function calls). For our game scripting code (that drives the whole game) it's very different. There we use coroutines and lambdas (our own, not the C++ ones) all over the place. That made us writing games much faster and safer. We already shipped four games with them and are very happy with the results. The trick is to pack coroutines into hierachical objects and only operate on their own data. That makes it easy and safe to reason about them and their lifetime. The development time savings of using them in our games were mind-blowing.

Bartman 22 April 2021 13:30

Here's an example of the generated assembly code from a trivial C++20 coroutine. Feel free to play around: Brown++ Compiler Explorer

matburton 23 April 2021 14:19

Thanks so much for this write-up!

I was expecting to read about graphical hijinx and assembly prowess, so C++20 and coroutines was a complete shock! :shocked

Quote:

Originally Posted by pink^abyss (Post 1478353)
There we use coroutines and lambdas (our own, not the C++ ones) all over the place. That made us writing games much faster and safer.

Quote:

Originally Posted by pink^abyss (Post 1478353)
That makes it easy and safe to reason about them and their lifetime.

I'm not one for hype, but since you used the words 'safe' and 'lifetime' what do you think about Rust? If the M68k LLVM backend becomes viable would it be appealing in any way?

pink^abyss 27 April 2021 13:21

Quote:

Originally Posted by matburton (Post 1478762)
I'm not one for hype, but since you used the words 'safe' and 'lifetime' what do you think about Rust? If the M68k LLVM backend becomes viable would it be appealing in any way?


My comment was about our work environment, where we don't use C++ for scripting. With 'safe' & 'lifetime' i don't mean it in a strict way like Rust guarantees it, but simply how it behaves in our console game projects.


All times are GMT +2. The time now is 17:53.

Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.

Page generated in 0.04676 seconds with 11 queries