Wiki

Clone wiki

agtools / Tutorial 2 - Playfields & The Arena

Outline:

ts2.png

tutorials/tutor2

You can build this example with:

> ./makedata.sh
> make clean; make

This step will introduce the playfield and arena interfaces. Like the shifter interface, these manage framebuffers for graphics display but with increasing levels of abstraction, each providing higher levels of functionality to manage the view.

Playfields:

A playfield is implemented through the playfield_template class. It provides storage and control over a single display memory buffer. However it also implements a lot of other stuff required to maintain a 'world' larger than the visible screen.

The playfield operates in terms of graphics tiles and a map specifying where each tile is used. Currently these can can be 16x16 or 8x8 tiles on STE.

A playfield can have 1 or 2 layers, with the second layer masked on top of the first using a chosen key colour (like sprites). Each layer can have its own independent map and tileset. Each layer is independently editable. Tiles may be edited while they are visible, using dynamic mode.

You can think of the playfield as a software implementation of an Arcade board (or console) character-mapped background.

The playfield also assumes responsibility for interacting with the shifter interface, to update the display address, linweidth, scroll etc. The direct calls we made in tutor1 will instead be handled by the playfield itself. Why is this so? Because managing the background layer dominates screen management. Stuff drawn on top of the background layer is always subordinate - i.e. relative to it.

The tiles and maps required by a playfield can be produced using agtcut with '--cutmode tiles'. See demos and tutorial scripts for examples. See agtcut usage guide for a lot more detail.

In this tutorial we're going to upgrade the previous example (tutor1) to use playfields in place of the raw memory buffer arrays we hacked up last time. There will exactly be one playfield per memory buffer.

The Arena:

The arena acts as a coordinator for playfields. It provides somewhat equivalent functionality to the hardcoded array of screenbuffer pointers we used in tutor1, and which we used to flip between front and back to achieve double buffering. e.g. this thing:

// define word buffers for a couple of hardcoded screens. add a single column of +16 to accommodate hscroll.
u16 screenbuffer0[SCREENWORDS];
u16 screenbuffer1[SCREENWORDS];

// an array of 2 pointers, one for each screenbuffer. used to manually select between them using index 0 or 1.
u16* screenbuffer_ptrs[] =
{
    screenbuffer0,
    screenbuffer1
};

Or the 68k equivalent:

screenbuffer0: ds.w SCREENWORDS;
screenbuffer1: ds.w SCREENWORDS;

screenbuffer_ptrs:
    dc.l screenbuffer0
    dc.l screenbuffer1

So why do we need another interface to wrap something as simple as an array of two pointers?

There are several good reasons:

The simplest being that we might have a very different mappings of framebuffers. The game may be triple-buffered - e.g. for a lower framerate RPG where you want to absorb 'steppy' slowdown (a hard drop from 50hz to 25hz is pretty rough - triple buffering gives you a near-continuous speed gradient).

The display may be configured single- or dual-field (colour interlace) which requires two playfields for each 'buffer'. The situation basically gets more involved as you introduce more concepts and relationships between buffers and display state. The arena manages all of this in a simple way, hiding a lot of the messy detail.

For now, it's just going to be operating in a simple front & back 2-buffer arrangement.

Since playfields are managed by and subject to the arena, most interactions with the background layer will be via the arena and not the individual playfield buffers.

Notes on configurability & performance:

Before we set up some playfield and arena definitions to use in our tutorial, it's useful to understand something about the way both of these are implemented.

Both classes are C++ templates. I have mostly avoided templates in this codebase to keep the amount of assumed knowledge to a minimum (and obscure templates require de-obfuscating documentation!). In some cases however the gains outweigh the 'learning curve inertia' they might cause. This is one of those cases.

C++ templates are powerful code-generators. When you write code in terms of templates, you are writing instructions for how to generate code - as opposed to writing the code directly. Although the way the syntax works does blur the lines to a degree.

The playfield is the best example here. When a playfield is defined, you are specifying the rules and limits for that 'kind' of playfield - as parameters. These rules then guide the code generation for the playfield rendering logic. This means each playfield 'configuration' is producing custom code. This greatly reduces the amount of logic which needs executed each game 'tick' just to support flexibility. The configuration feature is 'free'. However the usual cost is incurred for any kind of code generation - if you define 10 different kinds of playfield with different parameters, it generates (up to) 10 times as much 68k code.

Note: In the case of the playfield class, the amount of code generated isn't too big, but the amount of logic saved is quite significant. It should be ok to define multiple scrolling types in the same program without too much overhead.

Upgrading the code:

Next we'll see how to actually define a playfield type and the arena to group and use them. There's not a lot to it. While it has taken a lot of text to explain what (and why) they are - creating these things in a real program is quite easy.

First we need to add the necessary includes:

#include "agtsys/tileset.h"
#include "agtsys/worldmap.h"
#include "agtsys/playfield.h"
#include "agtsys/arena.h"

These define the following:

  • tileset.h : a tile library produced by agtcut (or pcs)
  • worldmap.h : a tile map produced by agtcut (or pcs)
  • playfield.h : implements playfield_template
  • arena.h : implements arena_template

Next we define our playfield configuration via playfield_template:

// define a playfield config for vertical-only scrolling
typedef playfield_template
<
    /*tilesize=*/4,
    /*max_scroll=*/8192,
    /*vscroll=*/VScrollMode_LOOPBACK,
    /*hscroll=*/HScrollMode_NONE
> playfield_t;

This basically means: "define a new playfield configuration type called 'playfield_t' with these parameters". You can then use 'playfield_t' anytime you want to create an instance of this scrolling configuration.

The parameters mean the following:

  • tilesize : sets the map tile size in 2^tilesize pixels. currently 4 (16 pixels)
  • max_scroll : indicates the maximum expected scroll distance for the maps to be used
  • vscroll : set the vertical scrolling behaviour (if any)
  • hscroll : set the horizontal scrolling behaviour (if any)

However since we're not dealing with playfield buffers directly, we define another object via arena_template which binds two instances of this playfield type together into a double-buffered arena which we can then control.

// define an arena (world) composed of 2 such playfields (double-buffered)
typedef arena_template
<
    /*buffers=*/2,
    /*singleframe=*/false,
    /*dualfield=*/false,
    /*playfield_type=*/playfield_t
> arena_t;

The parameters mean the following:

  • buffers : number of playfield buffers to bind
  • singleframe : a memory optimisation when using dual-field mode at 50fps (ignore for now)
  • dualfield : generate additional code required to use dualfield support
  • playfield_type : the playfield configuration to use for all buffers

There are some additional parameters which control more advanced features but we can ignore those until later.

A single instance of the arena_t type is now sufficient to implement our double-buffered scrolling world and provide coordination between the high-level game world, playfield map drawing and the low level shifter interface for display.

Assets:

Next we need to define asset objects for the playfield graphics.

// playfield tile assets
tileset mytiles;

// playfield map assets
worldmap mymap;

These are initially empty, but data can be loaded into them as we'll see next.

Misc:

We also want to set up some general purpose variables for scrolling.

// world map safety margin (in pixels) for speculative scrolling algorithm
static const int c_scroll_safety_margin = 16;

// viewport (scroll) starting position in world map
static s16 g_viewport_worldx = 0; 
static s16 g_viewport_worldy = c_scroll_safety_margin;

c_scroll_safety_margin specifies the closest we're allowed to get to the ends of the map on the scrolling axes only. For non-scrolling axes, we assume the full extents of the map are visible.

Why can't we scroll all the way to the ends of the map? The details are not too important for the purpose of the tutorial - but the scrolling algorithm is predictive (speculative) and it tries to paint slightly ahead of where the scroll is travelling. One tile ahead of whats potentially visible. Trying to view the first or last row of a vertically scrolling map technically renders something beyond the map extents, so we wish to avoid this. The same rule applies to horizontal scrolling but since that is disabled here, we don't need a margin.

*Note: This safety margin is actually a non-issue for some cases but rather than list all the cases where its ok and where its not, its better to assume its always present. It's easy enough to arrange in the source map and saves trouble debugging weird drawing glitches which may show up at an inconvenient time! *

Asset loading:

We're going to load our first assets (map and tiles) to use with our playfields. Assets are independent, so they can be managed and shared. They do however need attached to whatever is using them - in this case our playfield buffers.

First we have to pull in the files though. The file extension for tiles is '.cct'. For maps it is '.ccm'. Both are produced from a source image by agtcut. See other documentation for an explanation of these formats, if required.

mytiles.load_cct("tutor2.cct");
mymap.load_ccm("tutor2.ccm");

Determining the map size:

After loading the map, we can determine the map dimensions like this:

        s16 map_xtiles = mymap.getwidth();
        s16 map_ytiles = mymap.getheight();

        s16 map_xpixels = map_xtiles << 4;
        s16 map_ypixels = map_ytiles << 4;

Note: The tilesize shift of 4 (or multiply x16) to get pixel dimensions of the map can be obtained more correctly from the playfield itself, which allows simpler conversion of projects from 16x16 to 8x8 tile sizes:

        s16 map_xpixels = map_xtiles << playfield_t::c_tileshift_;

And then work out the safe scrolling limit from the map pixel size:

        // max scroll is map size minus screen dimensions (plus safety margin for the scrolling axes)
        s16 map_xlimit = (map_xpixels-320);
        s16 map_ylimit = (map_ypixels-SCREEN_LINES-c_scroll_safety_margin);

Create & initialise the arena:

So far we only 'described' the arena parameters for our scroll configuration and gave that configuration a name. We now need to create an instance of that configuration to use.

Each instance will create and maintain any playfields (and therefore any memory buffers) and state for one scrolling layer. We only need one instance for our example. We call our instance 'myworld'.

        arena_t myworld(shift, 320, SCREEN_LINES, /*dualfield=*/false);

The parameters passed to our arena 'myworld' are:

  • the shifter interface
  • the display width
  • the display height
  • the dual-field interlace switch

After creating our arena instance, we need to bind our assets to it (to the playfield buffers it is managing for us).

First we assign the map. The map is the same for all buffers (front and back).

        myworld.setmap(&mymap);

The tiles however can be distinct for each buffer so we loop over the arena buffer count and assign our tiles to each buffer in turn. The assignment order doesn't matter here. We will assign the same asset to both odd and even fields in normal mode. It will only use the first one in this single-field demo.

        for (int b = myworld.c_num_buffers_-1; b >= 0; b--)
        {
            myworld.select_buffer(b, 0);
            myworld.settiles(&mytiles, &mytiles);
        }

Now we're nearly ready to draw stuff. We set the initial scroll position using our local worldx, worldy coordinate variables, and init() the arena. This step fills the visible part of the arena buffers with tiles. 'visible' meaning the visible regions starting at our specified worldx,worldy.

        g_viewport_worldx = 0;
        g_viewport_worldy = map_ylimit;

        myworld.init(g_viewport_worldx, g_viewport_worldy);

Note that this is a vertical scrolling demo, and the map scrolls downwards. The 'viewport' moves upwards. So we start at the bottom edge of the map.

At this point I should make very clear that arena coordinates are world (or map) coordinates. Not screen coordinates. In fact nearly everything in AGT is in terms of world coordinates. To find screen coordinates at any time you must subtract worldx,worldy! Since most drawing is normally handled by AGT itself, this is more often the exception than the rule - but be aware of it.

The palette:

Last time we loaded random RGB colours. This time we want a real palette. Fortunately the .cct tile library carries the background's palette so we can load it from there.

        for (s16 c = 0; c < 16; ++c)
        {
            // logic (next)
            shift.setcolour(0, 0, c, mytiles.getcol(c));

            // physic (current)
            shift.setcolour(1, 0, c, mytiles.getcol(c));
        }

We have to load the palette for both shiftgroups before they are used, otherwise we'll get a strobing screen. Remember updates to shifter state are pending until committed. We want to prepare the palette for both the active and pending shiftgroups at the same time. These will persist for all future frames but can be changed at any time - typically by updating the next shiftgroup, which will be picked up on the next refresh.

Updating the mainloop:

We're almost done. In tutor1, we implemented buffer flipping directly using memory buffers. Here we instruct the arena which of its buffers is next to be the 'workbuffer' (or backbuffer) where things will be drawn.

        myworld.select_buffer(backbuffer_num, /*field=*/0);

We then update the arena's world coordinates:

        myworld.setpos(g_viewport_worldx, g_viewport_worldy);

...and request a draw update. This draws any tiles required for the change in coordinates - if any. If no change has occurred, nothing will be drawn. Each playfield holds its own state for draw optimisation.

        myworld.hybrid_fill(false);

Note: If 'true' is passed instead of 'false', it will force a re-fill of all visible tiles irrespective of coordinate changes.

Once drawing is complete, we ask the arena to present the current workbuffer as the new frontbuffer.

        myworld.activate_buffer();

That's basically all there is to maintaining a simple scrolling playfield using the arena interface. The remaining code just flips the front/back buffer index for next time, and moves the scroll coordinates.

        // toggle buffers so next time we write to the new backbuffer
        backbuffer_num = backbuffer_num ^ 1;

        // update the scroll coordinates
        g_viewport_worldy += scroll_speed;

        // if we hit the end of the map, turn around
        if ((g_viewport_worldy >= map_ylimit) ||
            (g_viewport_worldy <= c_scroll_safety_margin))
        {
            // change scroll direction
            g_viewport_worldy -= scroll_speed;
            scroll_speed = -scroll_speed;
            g_viewport_worldy += scroll_speed;
        }

Preparing the data:

The data for this example is built using the ./makedata.sh bash script. It is a very simple script with 2 commands. The commands themselves though need a bit of explanation.

The first is generating a 16 colour 'superpalette' from a selection of input assets. In this case, a single background image tutor2.png. The tool is pcs (PhotoChrome v6). You can use any other tool to produce a palette but you may have fun trying to get decent results ;) The reducer in pcs is designed for game palettes and while it is very slow, it is very adjustable and results can be far better than standard methods.

Note that superpalette production is only needed when your assets are not already mapped to a preferred 16 colours for your game. For the tutorials we're using a selection of non-Atari assets so reduction will normally be included in the demos.

../../bin/pcs -cd ste -ccmode 3 -ccfields 1 -ccrounds 10 -ccthreads 6 -ccsat 1.0 -ccincap 8192 -ccl 0=0:0:0 -ccl 15=f:f:f -ccpopctrl 0.5 -ccapc 1.0 -ccdpc 1.0 tutor2.png

The parameters (important ones) mean:

  • -cd ste : use STE palette resolution (versus STF)
  • -ccmode 3 : generate superpalette, from one or more input images
  • -ccfields 1 : produce normal, undithered palettes (2 for complementary-dithered palettes / dualfield graphics)
  • -ccrounds 10 -ccthreads 6 : time/effort to spend refining the palette. adjust to suit.
  • -ccl 0=0:0:0 : lock colour 0 to black
  • -ccl 15=f:f:f : lock colour 15 to white
  • -ccpopctrl 0.5 : set colour population curve - 1.0 favours common colours, 0.1 favours rare colours

This step produces 3 files:

chunky8.ccs
chunky8.ccr
chunky8.ccp

These carry the palette plus some other information used to remap the palette and to dither it where relevant (currently we don't care about dithering - its just the palette which will be used)

../../bin/agtcut -cm tiles -vis -t 0.0 -ccf 1 -bp 4 -om direct -s tutor2.png -p chunky8.ccs -o tutor2.cct

The parameters (important ones) mean:

  • -cm tiles : cut map and tile library (i.e. not sprites or anything else)
  • -vis : emit visualisation .tga of final map image (including any colour reduction taking place)
  • -t 0.0 : disable fuzzy tile matching
  • -ccf 1 : normal, single-field graphics (no interlace/dither)
  • -bp 4 : 4 bitplane tiles
  • -om direct : emit direct AGT formats (.cct, .ccm) and not some other format e.g. Degas PI1s or RB+
  • -s tutor2.png : source image of map to cut
  • -p chunky8.ccs : target palette to use
  • -o tutor2.cct : output tiles (+ map, will be named .ccm)

Having done this step, you should find the following files appear in your project dir:

  • chunky8.ccp : superpalette files
  • chunky8.ccr : ...
  • chunky8.ccs : ...
  • tutor2.ccm : map asset
  • tutor2.cct : tile library asset
  • tutor2_index.tga : visualisation/preview of tile numbers
  • tutor2_mapvis.tga : visualisation/preview of final (colour-reduced) map

Summary:

We should now see a vertically scrolling background which takes very little CPU time. This is a decent step forward, but there's still much to do before we have anything approaching a game.

We've spent quite a lot of text describing changes for tutor2, but if you take a look at the source you'll see the program is in fact quite short. We replaced some simple things with more sophisticated things and got a bit more functionality - but its almost the same amount of code in total.

Next time we're going to add sprites.

Updated