[WIP][Pine-2K][Game] ARIAT - HW

Introduction

ARIAT-HW is a spinoff of the main project I started for the Punk Jam and still haven’t released yet -thought secretly working on it.

Ultimately it’ll be a collection of various tiny games taking place in the same universe, and made with the amazing Pine-2K by @FManga.

I took interest into it because it’s great to prototype core gameplays.

This post also serves as a repository for various techniques I’m using.

Minigames & Experiments

Minigame - Highways

The intended game - it’s a prototype of the Highway gameplay from the main game.
It’ll be a sidescrolling road with a moving background and obstacles.

Status: Prototype-in-mind.

Minigame - Rumble

An arena where you’re in a tank and followed by a hoard of other tanks.

Status: In progress.

2020-07-24 - BennyHill
Sorry for that music

Techniques

Since 2K of code isn’t really much, I rely on the following techniques to help saving a bit of bytes.

Array & Pointer Arithmetics

Variables are great for 1-dimensional values, but I like to get 2 or 3-dimensional myself. Using more variables is just going to increase a lot the code, so how about packing them nicely into array instead?

const objectsCapacity = 5;
const objectSize = 3;
const objectsSize = objectsCapacity * objectSize * 4; // 32bits!
const objects = new Array(objectsCapacity * objectSize);
// objects' array is laid out like this:
// [update0, x0, y0, update1, x1, y1, ...];

var object0 = objects;
var object1 = objects + objectSize * 1;

// Iterating the objects.
for (var object = objects; object < objects + objectsSize; object++) {
  var objectUpdate = object[0];

  // Does this object have an update function?
  if (objectUpdate)
    objectUpdate(object);
}

...

function updateBarrel(object) {
  // 1 & 2 are x and y.
  sprite(object[1], object[2], BarrelSprite);
}

A collection of simple objects.

This allows me to have a dynamic number of objects. If you got only 2 objects, like the player and an enemy it’s probably not worth it.

Function Export / Import

An interesting technique which can help you pack a bit more code.

  • exec() allows you to execute another script.
  • save() to save an array into a file.
  • file() to load a file into an array in RAM.
  • The proc doesn’t mind executing any code at any location in the RAM.
  • Because Pine2K doesn’t check types, pointer arithmetics allows you to read any function’s bytes.

All of these combined mean you can use a script to export simple functions and load them from another script!

However, those functions come with a lot of limitations:

  • You must know in advance how many bytes the function will take.
    • What’s worst is that, while unlikely, code size may at time be greater in future versions of Pine2K.
    • I couldn’t find a reliable way to calculate it.
  • The function cannot call or use most symbols you’re declaring.
    • No function nor var.
    • const are probably ok, if they’re basic int or booleans. More testing to be done, but arrays are definitely out of the game.
  • The function can however call any standard Pine2K function, such as sprite() or exit().
  • Because it’s generated from a script, it will never get higher or equal to 2kB.
  • What it won’t eat in PROGMEM, it’ll eat in RAM.
    • While this is more abundant than PROGMEM, you might still need it for graphics & other data.

You can circumvent the call limitation by passing a function to be called (as a callback) or an array of functions.

I found the following uses for those:

  • Generic pure functions, such as abs or sqrti.
    • But don’t bother with abs/min/max, you’ll consume the same amount of PROGMEM at best.
  • Simplified functions which calls multiple system calls.
  • One-time script-like functions, like setups.
    • In my arena game, I’m going to have various positions for various objects - I could use an array of int and interpret it, or I could use this.

Also, there is a little trick due to Thumb-2 to make it work on the actual Pokitto (emulator is unaffected) (Thanks @FManga for the pointers!):

  • When writing the function to an array, you have to start 1 byte before.
  • Then, when loading it, you have to offset by +1 byte.
  • This is due to how the proc recognize Thumb-2.
// Exports a function into a file.
function exportFunction(func, size, filename)
{
    var arrayLength = (size + 3) / 4;
    var funcArray = new Array(arrayLength);
    
    func--; // Thumb-2 Trick.
    for (var iI = 0; iI < arrayLength; iI++) {
        funcArray[iI] = func[iI];
    }
    save(filename, funcArray);
    // This assumes you're not using auto-newlines (io/FORMAT).
    console("wrote "); console(arrayLength * 4); console(" into "); console(filename); console(" \n");
}

// Renders a sprite at the game position, with origin support and mirror on a sprite-basis, + recolor.
// position256 is assumed to be a pointer containing x, then y, whose value is actually a fixed24/8.
// spriteDef is a pointer containing the sprite, the origin in x, origin in y and a boolean of whether or not the sprite must be mirrored.
// recolor is the same recolor than sprite().
function drawSprite256(position256, spriteDef, recolor)
{
    mirror(spriteDef[3]);
    color(recolor);
    sprite(position256[0] / 256 - spriteDef[1], position256[1] / 256 - spriteDef[2], spriteDef[0]);
}
// Using PROGMEM, I know this function takes 108 bytes.
// This is tricky, because if future versions of Pine2K somehow makes this function bigger, only parts of it will be exported.
// It's probably better to include a few more bytes just in case.
exportFunction(drawSprite256, 108, "drSp256.fun");
// After this call, drSp256.fun will contain the compiled drawSprite256.

Code to export/import a function
// Imports drawSprite256 from the sprite.
const drawSprite256 = file("drSp256.fun", null) + 1;
// PROGMEM will be less affected, however the main RAM's consumption will increase.

...

// An object updated with updateObject(), at the middle of the screen.
var object = [updateObject, 110 * 256, 88 * 256];
// A debug circle whose origin is at 6, 6, and not mirrored.
const DebugCircleSprite = [file("DebugCircle", 0), 6, 6, false];
// X/Y starts at 4.
drawSprite256(object + 4, DebugCircleSprite, 6)
Code to export/import a function
5 Likes

Great write up of the techniques you are using - will definitely help others!

2 Likes

I love seeing ARIAT story grow farther than just one game :slight_smile: hope to see more.

1 Like

Small update about the rumble!

I’ve been tilemaps and making them scrollable.
The tilemap itself is just a png, it’s using 2 16x16 tiles too.
I also played with window a bit to circumvent a limitation of tileshift (which clamp to 0-7, insufficient for 16x16 tiles).

2020-07-26 - Tilemap16
I’ve been craving chocolate recently, don’t mind the colors. Also, I forgot to rename the text - it’s not tileshift but updateTilemap16.

I’ve also been trying to integrate it into the rumble minigame, but unfortunately I think I ran out of global variables, so I need to pack things tighter before anything else.

Additionally I made a camera offset so it could be used with this tilemap, but it’s hard to demonstrate right now.

5 Likes

More tilemap fun and corruption too

  • I’ve transformed a bit my rumble script to liberate a few “symbols”.
  • I implemented the tilemap rendering on Rumble
    • As my tilemap is actually a 8bpp bitmap, I can render it directly as a minimap, which is done by maintaining B.
  • I experienced various corruption issues (most likely due to my uses of exported functions or/and pointer arithmetics) so I ended up having weird fixes in the code.
    • The whole thing is very likely to break badly when Pine gets an update.

2020-07-28 - Race & Tilemap
The rumble with minimap showing when B is pressed and everything is happening normally.

2020-07-28 - Corruption
The other rumble with the corrupted map.

2020-07-28 - Minimap Corrupted
The minimap also gets corrupted. The large band of noise seems to be the music’s buffer (512 is 12.5% of 64x64 bytes, and it gets updated less often than the rest of the game or corruption).


On the device, some corrupted tiles will restart Pine with a different font - music was also killed in the process, but otherwise the game is running fine.

Gameplay-wise, it feels fit for the racing-like gameplay of the Highways sections that’ll be later included in ARIAT. The tilemap helps a lot getting a notion of movement since the camera is now following the player.

The controls feel nice as well, I’m growing fond of it.

7 Likes

Sprites manipulation, linefillers and an exploration of the “road renderer”

  • I had fun with giants sprites and io("SCALE", 2), and emulated the LoRes 256 buffer.
    • At first messing up with colors.
  • Then I started created a “road renderer”, like Road Rash and similar game using a sprite as a the frame buffer.
    • It might be what will become the highways gameplay in ARIAT in the end.
    • I used a 110x88 sprite and updated it with a bit of maths to make it convincing.
    • I also used soft gray colors for faraway marks.
  • @FManga pointed out that filler was also working with custom functions, opening the door to custom linefillers.
    • It’s using the same linefiller(buffer, y, skip) signature.
    • You can replace a filler in a given slot.
    • I replaced the TILES filler with mine with a filler call.
    • It is much smoother than with a giant sprite, which is expected.
  • After porting the code to an actual linefiller, I drawn some sprites in order to add objects.
    • Basically bill boards and pine trees.
    • I used io("SCALE", 2) again so closer sprites wouldn’t be as costly in ram.
    • For faraway objects I used colored pixels.

2020-07-30 - rainbow 2020-07-30 - rainbow NB 2020-07-30 - rainbow 4b
Using a sprite as a frame-buffer - and a x+y+random() as the color. Miloslave is great for these. First one is 8bpp, low-res. Second one is 1b, hires. Third one is 4b, hires.

2020-07-30 - road
A “low-res” used with a simple road renderer. It’s a bit blocky.

2020-07-30 - road with items
Final result - linefiller-based road renderer, and scaled sprites. Don’t ask about why trees pierced the road. It’s smoother on the actual device too.

It feels satisfying. A challenge in integrating a road renderer in ARIAT is that it’s supposed to be happening in a city with tall buildings everywhere, so I’ll have to think about that somehow.

Also, I’d like to introduce a proper curve for the road, with slight bumbs too. And an actual gameplay too!

5 Likes

Less things today

  • I’ve come up with a structure to more easily draw billboard-type sprites and applied it to the two objects.
    • Basically it’s a list of sprite, with the apparent size.
    • When rendering a billboard, an ideal size will be computed, then the list will be checked once until a good sprite is found.
    • If the end of the list is reached, it’ll be checked a second time, but while taking in consideration that the sprite will be scaled x2.
    • This will get easier on the progmem.
  • I’m having a unit issue.
    • Game-wise, each object has a x (horizontal position) and z (depth position) coordinates.
    • X goes from -110256 to 110256, but each band is of 128 length in z.
    • I tried to fix that today but to no avail.
  • I also noticed minor clipping issues with scaled sprites.
  • Lastly, I applied the trick from @FManga to sort objects - that is, doing it by swapping two objects if they’re not correctly ordered.
    • It’s great until you travel too fast (which shouldn’t happen later).
  • I’m impressed that with ~64 sprites + the filler it’s still pretty smooth on the pokitto.

2020-07-31 - Unsorted2020-07-31 - Sorted
Left is unsorted, right is sorted. Looks much better!

2020-07-31 - 96
Lots of objects!

Upcoming things:

  • Making game units more consistent.
  • Introducing an actual curve for the track.
  • Adding a vehicle maybe
5 Likes

This looks so good!

1 Like