The way your game will behave is configured in config.h
via the definition / undefinition of a bunch of compiler directives and constants.
This is a short reference.
//#define GAME_AREA_TOP
#define GAME_AREA_MIDDLE
//#define GAME_AREA_BOTTOM
//#define GAME_AREA_CUSTOM
Leave three commented, one uncommented. Defines where the game area is located, vertically, on screen. There are three predefined locations:
Places the game area at the top of the screen. There's room for a 3-4 lines tall hud at the bottom of the screen, from Y = 26 to Y = 29.
Places the game area vertically centered. There's room for a couple of lines above and a couple of lines below the game area for your hud (Y= 2, 3 and Y = 28, 29).
Places the game area at the bottom of the screen. There's room for a 3-4 lines tall hud at the top of the screen, from Y = 2 to Y = 5.
Besides, there's a custom location:
Just place manually at autodefs.h
:
#ifdef GAME_AREA_CUSTOM
#define TOP_ADJUST 6
#define SCROLL_Y 12
#endif
Used to define the size of the map (or the biggest map in the set if your game has several levels):
#define MAP_W 3
#define MAP_H 3
Define player's initial life gauge (and MAX!), plus the amount of life points recovered when getting a life refill (hotspot type 3):
#define PLAYER_LIFE 5
#define PLAYER_REFILL 1
For multilevel games, uncoment MULTI_LEVEL
and set MAX_LEVELS
with the amount of levels.
Single level example:
//#define MULTI_LEVEL
#define MAX_LEVELS 1
Multiple level example:
#define MULTI_LEVEL
#define MAX_LEVELS 5
If you are creating a single level game, you must give proper values to this set of constants:
#define SCR_INI 0 // Initial screen
#define PLAYER_INI_X 2 //
#define PLAYER_INI_Y 2 // Initial position
//#define SCR_END 99 // Final screen, undefine if N/A
#define PLAYER_END_X 99 //
#define PLAYER_END_Y 99 // Ending position
#define PLAYER_MAX_OBJECTS 1 // Objects to finish game
Those constants are ignored if you defined MULTI_LEVEL (in such case, values are extracted from assets/levelset.h
).
In the next section, you give actual numbers to every hotspot type you are using. Comment out the hotspot types you are not using so you save space.
Basic hotspot types are collectible items, keys and refills. As mentioned, if you are not using any of such kinds of hotspots, just comment out the #define:
#define HOTSPOT_TYPE_OBJECT 1
#define HOTSPOT_TYPE_KEYS 2
#define HOTSPOT_TYPE_REFILL 3
//#define HOTSPOT_TYPE_AMMO 4
//#define HOTSPOT_TYPE_TIME 5
Note that the numbers here are those you use in ponedor when placing the hotspots, so there's room for some customization. The numbers are also used to render the objects on screen: the number is also an index to the spr_hs
metasprite array defined in assets/metasprites.h
.
You may add code to support more kinds of hotspots. You can also use resonators, which are kind of custom but are kind of built in (!):
//#define HOTSPOT_TYPE_RESONATOR 4
//#define HOTSPOT_TYPE_RESONATOR_ON 5
A level (or the game, in single level games) is won if (in order of importance):
- If scripting is enabled (see
ACTIVATE_SCRIPTING
below), ifWIN GAME
was executed in the level script. - If
PLAYER_MAX_OBJECTS
is set, when the player gets all hotspots ofHOTSPOTS_TYPE_OBJECT
type in the level. - If
SCR_END
is defined, when the player hitsPLAYER_END_X
,PLAYER_END_Y
inSCR_END
.
To define a customized "win level" condition, just define WIN_LEVEL_CUSTOM
. Once you have, the level will be won if and only if the variable win_level
is set.
It's up to you to set win_level
upon meeting a custom condition. You can do it in my/extra_checks.h
for example.
There are several map formats. You may define one and only one format, leaving all others commented.
//#define MAP_FORMAT_PACKED
//#define MAP_FORMAT_RLE16
#define MAP_FORMAT_RLE53
//#define MAP_FORMAT_RLE44
//#define MAP_FORMAT_RLE53_CHRROM
//#define MAP_FORMAT_RLE44_CHRROM
This is a legacy format. Each tile takes one nibble. Screens are 16x12 = 192 tiles so they take 96 bytes each. As nibbles are used to store tile indexes, the maximum number of different tiles is 16.
To convert .MAP files into packed maps use mapcnvnes2
.
If you use decorations (i.e., tiles out of range which are stored separately) they use the old format (which needs a pointer index taking 2*MAP_W*MAP_H
bytes).
This is a legacy format. Map screens are stored using a not very efficient RLE algorithm.
To convert .MAP files into RLE16 maps use legacy/rlemap2
.
If you use decorations (i.e., tiles out of range which are stored separately) they use the old format (which needs a pointer index taking 2*MAP_W*MAP_H
bytes).
Maps are stored using RLE. The 53 variant uses 5 bits for tile number and 3 bits for run length, which allows for 32 different tiles. The 44 variant uses 4 bits for tile number and 4 bit for run length. If you have 16 different tiles or less, 44 compresses a tad better.
To convert .MAP files into RLE53 maps use rle53mapMK1
. To convert .MAP files into RLE44 maps use rle44mapMK1
.
If you use decorations (i.e., tiles out of range which are stored separately) they use the new format which doesn't require an extra bytes (they take less space!).
Exactly like MAP_FORMAT_RLE53
and MAP_FORMAT_RLE44
, but read from CHR-ROM
instead.
The map format is the same, but they require quite a setup. First of all, you should create binaries using rle53mapchrrom
or rle44mapchrrom
which generate CHR-ROM-ready images, plus an index and a bunch of constants. Please check this for more info on the utilities, and Cheril the Writer to see CHR-ROM based maps in action.
A "Decoration" is a tile which index is out of bounds. Imagine that you are using RLE53 and have 32 tiles maps. You can have more than 32 tiles for casual decorations. When the converter finds a tile with index >= 32, it stores it separately, in a "decorations" array.
Decorations are good for exclusive stuff. Don't overuse them, as they can take up quite a bit of space.
If you use decorations in your maps, activate support for them:
#define MAP_WITH_DECORATIONS
Decoration arrays are exported alongside maps. You have to reference them in your level asset arrays (assets/levelset.h
).
If you enable the complex renderer,
#define MAP_RENDERER_COMPLEX
Each screen is decoded to a buffer, then custom code is injected, then the screen is painted using the contents of such buffer. That means that you can alter the contents of the buffer in the injected code.
The decoder writes the screen into the map_buff
array. Such array contains 192 bytes, one per tile, in row order, from top to bottom and left to right. That means that map_buff [i]
contains the tile at Y = (i >> 4)
, X = (i & 0xf)
.
Usually, in my/map_renderer_customization.h
you iterate the values of map_buff
and modify them if they meet certain conditions.
This allows for much more complex visuals, and also to modify the screen contents if some conditions are met.
The possibilities are endless. Check the examples and testers to see this in action.
The engine allows for some customization of player and enemies bounding boxes.
Theres a bounding box for collisions with the background an a different one for collisions with other actors. Both bounding boxes are 8 pixels wide. The base height is 16 pixels but can be modified using a couple of constants.
8 pixels wide
O-----+
| |
| |
| | 16 pixels tall
| |
| |
| |
+-----+
O is the origin of coordinates (prx, pry
). The value of the constants moves the top edge of the box up (positive values) or down (negative values). Note that the origin O is not moved, just the collision box edge!
Example 1, a value of 4 moves the top edge UP:
8 pixels wide
+-----+ \
| | 4 pixels stretch |
O·····+ |
| | |
| | |
| | 16 pixels tall | 20 pixels tall overall
| | |
| | |
| | |
+-----+ /
Example 2, a value of -4 moves the top edge DOWN:
8 pixels wide
O······
· · 4 pixels shrink
+-----+ \
| | 16 pixels tall |
| | |
| | | 12 pixels tall overall
| | |
+-----+ /
The constants are:
#define PLAYER_COLLISION_VSTRETCH_BG -4
#define PLAYER_COLLISION_VSTRETCH_FG -4
The first extends the collision box for collisions with the background. The second extends the collision box for collisions with other actors. Usable values range from -8 to 8.
It works like the player's, but is only used for collisions with other actors (player / bullets / hitter):
#define ENEMS_COLLISION_VSTRETCH_FG 0
Push boxes are boxes which can be pushed around. This works better for top-down games are pushable tiles don't get affected by gravity in side-view games.
Define PLAYER_PUSH_BOXES
to enable this feature:
#define PLAYER_PUSH_BOXES
If PLAYER_PUSH_BOXES
is defined, the behaviour can be further configured:
#define FIRE_TO_PUSH
If defined, player has to push B besides the D-PAD to push a pushable block. If commented out, D-PAD is enough.
Pushable boxes can be used alongside the scripting system. To enable the required hooks in the code, just define:
#define ENABLE_PUSHED_SCRIPT
When ENABLE_PUSHED_SCRIPT
is defined, further constants are used to configure the behaviour:
#define PUSHED_TILE_FLAG 1
#define PUSHED_TO_X_FLAG 2
#define PUSHED_TO_Y_FLAG 3
The values to these three constants are indexes in the flags
array. If ENABLE_PUSHED_SCRIPT
is defined, when a player pushes a pushable block, the overlapped tile is copied to flags [PUSHED_TILE_FLAG]
, and the end coordinates of the pushable block are copied to flags [PUSHED_TO_X_FLAG]
and flags [PUSHED_TO_Y_FLAG]
. Then, the PRESS_FIRE
scripts are run.
If you are not using keys/bolts and/or collectible items, besides leaving out the related HOTSPOT_TYPE_*
, you should uncomment one or both of those:
//#define DEACTIVATE_KEYS
//#define DEACTIVATE_OBJECTS
When hit, the player can bounce off the offending peril:
#define PLAYER_BOUNCES
#define DOUBLE_BOUNCE
If DOUBLE_BOUNCE
is on, the bounce is stronger.
It can also be relocated to a safe spot:
#define DIE_AND_RESPAWN
#define DIE_AND_REENTER
If DIE_AND_REENTER
is defined alongside DIE_AND_RESPAWN
, the screen is reloaded even if the respawn point is in the same screen.
PLAYER_BOUNCES
are DIE_AND_RESPAWN
are practically exclusive: if you enable both, you won't see the bounce as the player will be relocated.
You can also make the player flicker for a short while after being hit. This configuration is compatible with both PLAYER_BOUNCES
and DIE_AND_RESPAWN
.
#define PLAYER_FLICKERS
Usually, patrollers walk from (X1, Y1) to (X2, Y2) and back from (X2, Y2) to (X1, Y1), or bounce in the rectangle described by (X1, Y1) and (X2, Y2) if both X1 != X2 and Y1 != Y2. If you define
#define WALLS_STOP_ENEMIES
Obstacles will make enemies bounce as well.
By default, the engine supports four basic tile behaviours:
- Empty, or behaviour 0.
- Evil, or behaviour 1. They kill the player.
- Obstacle, or behaviour 8.
- Platform (only in side-view), or behaviour 4. Like obstacles, but only stop the player when falling.
Tile behaviours, or "behs", are defined in assets/behs.h
. Basicly you define a behaviour for each tile in the tileset for each level in your game.
If you are creating a side-view game and all "evil" tiles are spikes or lava pits, you can save some space by defining
#define NO_HORIZONTAL_EVIL_TILE
As the engine will only check the collision against evil tiles while moving vertically.
Extra tile behaviours can be added by enabling several extra modules:
#define ENABLE_QUICKSANDS
If defined, tiles with beh == 2 are considered "quicksands". They will make the player sink slowly.
#define ENABLE_BREAKABLE
If defined, tiles with beh & 16 are considered "breakable" (note that & 16 means that it can be combined with other behaviours: for example 8|16=24 for a breakable obstacle, or 1|16=17 for a breakable killable, like the barb wire in Sgt. Helmet's Training Day).
If ENABLE_BREAKABLE
is defined, the behaviour of this module can be further configured:
#define BREAKABLE_LIFE 2
The amount of hits for the breakable tile to break completely. If BREAKABLE_LIFE
equals 1 the RAM requirements and the amount of code involved are MUCH lower, so think if you really need several hits to break breakables ;-) (basicly, you save 192 bytes of RAM and a couple 100s of bytes of ROM).
#define BREAKABLE_ANIM
Animate breaks. If undefined, broken tiles will simply disappear. If defined, an intermediate, broken representation of the tile will be shown for a while, then cleared. This needs further configuration:
#define BREAKABLE_MAX 4
Max. number of simultaneous break animations. 4 seems a good number but you may need more. The optimal value depends on how long the "animations" are and how you break tiles (by shooting, for example, with lots of simultaneous bullets). The only way to actually find the best value is by trial & error ;-)
#define BREAKABLE_MAX_FRAMES 8
Number of frames to show the "broken" representation of a tile which is about to disappear.
#define BREAKABLE_ERASE 0
"Disappear" means replacing the broken tile with tile number BREAKABLE_ERASE
#define BREAKABLE_BREAKING 8
This constant defines which tile in the tileset contains the "broken" representation of the breakable tile.
#define BREAKABLE_WALKABLE
If BREAKABLE_WALKABLE
is defined, breakable tiles can be broken by walking on them. See tester sideview for a nice example.
(only for side view)
#define ENABLE_CONVEYORS
Conveyor belts will displace the player left or right if he or she lands on a tile marked as "conveyor". Conveyor belts are 40 (pushes left) or 41 (pushes right).
(only for side view)
#define ENABLE_SLIPPERY
A tile with beh "obstacle" (8) or "platform" (4) can be made "slippery" just adding 64 (beh == 72 or 68). Slippery floors decrease acceleration and friction (instead of PLAYER_AX
and PLAYER_RX
, constants PLAYER_AX_ICE
and PLAYER_RX_ICE
are used).
(only for side view)
#define ENABLE_LADDERS
Tiles with beh == 32 are ladders. Check tester punchy or Cheril the Writer's last level.
(only for side view)
Propellers are animated fans (or whatever) which create a column of tiles which make the player "float". The height can be configured. Obstacles and platforms block propellers.
#define ENABLE_PROPELLERS
Configuration:
#define PROPELLERS_MAX 4
Max number of simultaneous propellers on the same screen. Please adjust this number to the minimum possible value.
#define PROPELLERS_BASE_PATTERN 64
Propellers are animated using 4 patterns to create two 8x16 pixels animation "cells". Such patterns should be contiguous and start at index PROPELLERS_BASE_PATTERN
. Check Cheril the Writer.
#define PROPELLERS_MAX_LENGTH 6
Maximum height of the area affected by a propeller. Note that obstacles and platforms actively "block" propellers.
#define PROPELLER_TILE 14
The engime will place a propeller on screen everytime it founds the PROPELLER_TILE
while rendering the current screen (room).
#define PROPELLERS_ON_BY_DEFAULT
Propellers only work if propellers_on
is set. If you define PROPELLERS_ON_BY_DEFAULT
, propellers_on
is set by default. Otherwise it isn't and you have to. This is what happens in Cheril the Writer.
#define ENABLE_SHINES
Shines are just cosmetic. They make evil tiles shine randomly, from time to time, using two consecutive patterns in the sprite bank.
#define SHINES_MAX 8
Maximum amount of shines on the same room.
#define SHINES_BASE_PATTERN 10
Base pattern in the sprite bank (bank 1)
#define SHINES_PALETTE 3
Use this palette to draw the sprites.
#define SHINING_TILE 23
As with propellers, everytime the renderer finds a tile SHINING_TILE
it will create a shine for current screen.
Not really interesting. They are used in Cheril Perils Classic and Cheril the Writter
They can make appear a spike or some sort of killing tile if the player steps on or jumps over a certain tile index, like a booby trap.
#define ENABLE_SPRINGS
Configuration:
#define SPRING_TILE 10
SPRING_TILE
is the number of the tile the engine should detect as "spring".
#define SPRING_SPIKE_TILE 11
SPRING_SPIKE_TILE
is the number of the tile which appears once the spring trap is activated.
#define SPRINGS_NEED_POSSEE
If defined, SPRINGS_NEED_POSSEE
means that the player has to actually land on the tile to activate the trap. Otherwise the trap appears once the player overlaps the tile cell above the trap.
#define SPRINGS_ON_BY_DEFAULT
Springs only work if the variable springs_on
is set. If SPRINGS_ON_BY_DEFAULT
is defined, springs_on
is set automaticly.
This kind of booby traps are used in Cheril Perils Classic. In this game, SPRINGS_ON_BY_DEFAULT
is undefined. Via custom initialziations in my/extra_inits.h
, springs_on
is 1 only on level 1.
Warpers are entities which, when interacted with, take the player elsewhere. They are placed as enemies with ponedor as type FF. attr
is the destination screen (room) number, and s1
should contain the destination coordinates (packed 0xYX).
#define ENABLE_SIMPLE_WARPERS
Configuration:
#define SIMPLE_WARPERS_BASE_SPRID (32+((frame_counter>>2)&3))
Warpers are rendered using metasprites in the active spr_enems?
array. SIMPLE_WARPERS_BASE_SPRID
is used to determine which metasprite. It can be an expression, like the example above, which cycles four different cells from index 32 using frame_counter
divided by four (one cell change every four frames).
#define SIMPLE_WARPERS_FIRE_BUTTON
Define this if you require the player to press B to activate the warper. If it is undefined, the warpers will be activated just by touching them.
#define ENABLE_NO
This feature was designed to show a small text balloon with a "NO!" icon above the player for a while. It is triggered when
- The player attempts to open a lock without a key.
- Using easy objects, the player attempts to use an item in the wrong hotspot.
It can be triggered anywhere just by setting no_ct
with a frame count (50 = 1 second).
#define NO_METASPRITE ssit_06
#define NO_OFFS_X 0
#define NO_OFFS_Y -24
The metasprite used is NO_METASPRITE
, at (prx + NO_OFFS_X, pry + NO_OFFS_Y)
.
#define ENABLE_USE_ANIM
The "use animation" is a subset of cells in the spr_player
array which will be started once the player:
- Interacts with a hotspot.
- Presses B to run a fire script (when using scripting).
- Interacts with an interactive.
The actual "action" will be performed when the sequence reaches a determined value:
#define USE_ANIM_MAX_FRAMES 13
#define USE_ANIM_INTERACT_ON 7
#define USE_ANIM_FRAMES_PER_STEP 4
The animation counter, or use_ct
, will iterate from 1 to USE_ANIM_MAX_FRAMES
if the interaction was successful, or from 1 to USE_ANIM_MAX_FRAMES + 1
(skipping USE_ANIM_MAX_FRAMES
) if it wasn't.
Each cell will be displayed for USE_ANIM_FRAMES_PER_STEP
frames but the last, which will be displayed for one second.
You are in charge of preparing your my/player_frame_selector.h
to select the correct cell from spr_player
if use_ct
is != 0. If CELL_USE
is the first frame in the sequence, then:
if (use_ct) {
psprid = CELL_USE + use_ct - 1;
} else // ...
use_ct
should have priority over other player states in my/player_frame_selector.h
. Check those games for inspiration:
#define ENABLE_TIMER
The timer starts from the initial value and is decremented each second. When it reaches zero, a flag variable is risen (but you are in charge to zero it back!) and, if scripting is active, the ON_TIMER_OFF
section is executed.
#define TIMER_INITIAL 99
#define TIMER_START_ON
#define TIMER_REFILL 30
TIMER_INITIAL
is the timer's initial value. When it reaches zero, it will cycle back to TIMER_INITIAL
.
The variable timer_on
determines if the timer is running or not. TIMER_START_ON
makes timer_on = 1
when the game starts. If it is left out commented, you are in charge of starting the timer.
TIMER_REFILL
is the amount of seconds added to the timer if the player gets a HOTSPOT_TYPE_TIME
.
#define TIMER_RESET_ON_ENTER
#define TIMER_SOUND 10
If TIMER_RESET_ON_ENTER
is defined, the timer will be reset every time the player enters a new screen. TIMER_SOUND
, if defined, will play a special sound if timer < the defined value.
Deprecated (because they are pretty useless):
#define TIMER_TIME_FLAG 0 // Useful with scripting. Copies time to flag
#define TIMER_ZERO_FLAG 1 // Useful with scripting. raises flag when time zero
#define ENEMS_IN_CHRROM
Enemies are stored somewhere in CHR-ROM. You must tell the engine where in assets/levelset.h
. The arrays l_enems_chr_rombank
, l_enems
and l_hotspots
should contain valid data for every level in the game:
l_enems_chr_rombank
contains which CHR-ROM bank contains the enemies + hotspots data for each level.l_enems
is the address of the enemy data for each level, in PPU addresses.l_hotspots
is the address of the hotspots data for each level, in PPU addresses.
If you use eneexp3
in bin mode and binpaster
to glue all binaries together, you should have offsets to the beginning of the enemy block (ENEMS?_H_BIN_OFFS
) and offsets to the beginning of the hotspots block (ENEMS0_?_BIN_OFFS + HOTSPOTS_OFFSET_?
).
A more detailed explanation is comming in form of tutorial in the near future. You can check Cheril the Writter meanwhile.
#define ENEMS_LIFE_GAUGE 1
Amount of shots/punches/kicks needed to kill enemies.
#define ENEMS_FLICKER
If ENEMS_FLICKER
is defined, enemies will flicker for a while when hit. If it is left commented out, a explosion will show instead.
#define ENEMS_FLICKER_ONLY_ON_DYING
If defined, the flicker will only happen on the last hit (if ENEMS_LIFE_GAUGE
> 1, that is).
//#define ENEMS_CAN_RESPAWN
Not yet complete. Needs documentation + further tests.
#define PERSISTENT_ENEMIES
#define PERSISTENT_DEATHS
Those two are important.
PERSISTENT_ENEMIES
makes the engine remember the position and direction of enemies when you exit the screen and come back later. It may be needed for some gameplays, but be warned that it eats RAM as it needs 4 bytes per enemy in the level, which meansMAP_W*MAP_H*4*3
bytes.PERSISTENT_DEATH
remembers which enemies are killed. If not defined, enemies will respawn every time you reenter a screen. It takesMAP_W*MAP_H
bytes.
#define ENEMS_TOUCHED_FRAMES 8
Stay "touched" (blinking or with the explosion sprite) for ENEMS_TOUCHED_FRAMES
frames.
#define ENEMS_RECOIL_ON_HIT 2
#define ENEMS_RECOIL_OVER_BOUNDARIES
If ENEMS_RECOIL_ON_HIT
is defined, enems will recoil a bit when hit. For every frame they are in the "touched" state, they will recoil ENEMS_RECOIL_ON_HIT
pixels.
For linear enemies, the (X1, Y1) and (X2, Y2) boundaries will stop the recoiling. define ENEMS_RECOIL_OVER_BOUNDARIES
if you don't want this to happen.
#define ENEMS_ENABLE_DYING_FRAME
If defined, when enemies are hit, the "dying frame" cell is displayed. Remember that, generally, enemies' metaspritesets are:
RIGHT_WALK_1, RIGHT_WALK_2, RIGHT_HITTING, RIGHT_DYING,
LEFT_WALK_1, LEFT_WALK_2, LEFT_HITTING, LEFT_DYING
If you are using explosions (ENEMS_FLICKER
is undefined):
#define ENEMS_EXPLODING_CELL 32
#define ENEMS_EXPLODING_CELLS_HIDES
The value of ENEMS_EXPLODING_CELL
refers to the index in the spr_enems?
arrays of a explision metasprite. If ENEMS_EXPLODING_CELLS_HIDES
is left commented out, the explision overlaps the enemy sprite. If it is defined, it substitutes it.
The enemies types "Saw" and "Pezons" eventually emerge from behind the background. To achieve the effect, the "occluding sprite" trick is used. The occluding metasprite should be in the spr_enems?
arrays. Its index should be programmed into this constant:
#define ENEMS_OCCLUDING_CELL 33
Cheril the Writter implements saws, for example. You can see the occluding frame in the spriteset next to the saw.
mkts
will make metasprites low priority if the colour #FF00FF is found in the graphic. #FF00FF always encodes as colour 0 (background), by the way.
#define ENEMIES_SUFFER_ON_PLAYER_COLLISION
Substracts life from enemies when they collide the player. Warning! This will only work if the engine is prepared for enemies to die, this is, if the player shoots, can step on enemies, or hits.
Fanties (type 6 enemies) hover around chasing the player. Homing Fanties only do it if the player is close enough, tho, and return "home" (the starting position) otherwise:
#define ENABLE_FANTY
#define ENABLE_HOMING_FANTY
Define only one. You can't have both.
Fanties are further configured by:
#define FANTY_BASE_SPRID 32
The base index in spr_enems?
.
#define FANTY_WITH_FACING
If defined, Fanty will look left / right. You will need four cells for fanties in spr_enems?
:
RIGHT_FANTY_A, RIGHT_FANTY_B, LEFT_FANTY_A, LEFT_FANTY_B
The engine will alternate between A & B for each direction while Fanty moves around. If FANTY_WITH_FACING
is undefined you just need two cells.
#define FANTY_COLLIDES
Leave this undefined if you want Fanties to fly through walls.
#define FANTY_KILLED_BY_TILE
If defined, Fanties can be killed by killing tiles (beh & 1).
#define FANTY_LIFE_GAUGE 5
If defined, Fanties will have their own life gauge. If not defined, fanties will share the same life gauge as the rest of the enemies.
#define FANTY_A 4
#define FANTY_MAXV 48
Movement values for acceleration and maximum velocity. The lower the acceleration, the slower the fanty will react to the player movement.
Only for Homing Fanties:
#define FANTY_DISTANCE 80
#define FANTY_V_RETREAT 16
Fanties will only notice the player if it is closer than FANTY_DISTANCE
in pixels. FANTY_V_RETREAT
is the speed at which Fanties return "home".
#define ENABLE_PURSUERS
Type 7 enemies. Only really usable in top-down games. Pursuers are spawned from the initial position and chase the player. If the player kills a pursuer, a new one will be spawned after a while. Further configuration:
#define DEATH_COUNT_EXPRESSION 50+(rand8()&63)
When a pursuer is killed, the next one will be spawned after DEATH_COUNT_EXPRESSION
frames. Of course (as pictured) you can use a complex expresion or a constant number.
#define TYPE_7_FIXED_SPRITE 4
If defined, all spawned enemies are of type TYPE_7_FIXED_SPRITE
. Otherwise, they can be 1, 2, 3 or 4 at random.
#define ENABLE_SAW
Saws (type 8 enemies) have 4 states: emerging, advancing, hiding, retreating. They can kill the player only while emerging. You define the travelling direction with start/end points in ponedor. They will emerge left or up if the attr is 0, or right or down if it is 2.
#define SAW_BASE_SPRID 48
The base index in spr_enems?
. Saws need two cells which alternate very fast.
#define SAW_V_DISPL 4
Speed when advancing or retreating, in pixels per frame.
#define SAW_EMERGING_STEPS 10
How many pixels saws pop out.
#define ENABLE_PEZONS
Pezons (Type 9) are usually fish which jump out of water or fire balls which jump out of lava. The frequency is defined in the attr field in ponedor.
#define PEZONS_BASE_SPRID 40
The base index in spr_enems?
. Pezons need two cells: "up" and "down". For fish or jumping animals, "mouth open" and "mouth closed" works quite nicely. For fire balls, "trail down" then "trail up" also looks great.
#define PEZON_WAIT 50
Base idle time. The value of the attr field in ponedor multiplied by 8 will be added to PEZON_WAIT
.
#define PEZON_THRUST 384
#define PEZON_VY_FALLING_MAX 256
#define PEZON_G 16
Movement values (Initial VY when jumping, maximum VY when falling and gravity, respectively).
The support for "chac chacs as enemies" is deprecated. It's still there 'cause this was the original implementation.
#define ENABLE_MONOCOCOS
Monococos (Type 0xB enemies) are hiden, appear from time to time, shoot a coco to the player, then disappears again. Monococos cycle through 4 states:
- Idle, for A frames.
- Appearing, for B frames.
- On, it shoots a coco, then waits for C frames.
- Disappearing for B frames.
The values of A, B, C are defined using these constants:
#define MONOCOCO_BASE_TIME_HIDDEN 150 // A
#define MONOCOCO_BASE_TIME_APPEARING 50 // B
#define MONOCOCO_BASE_TIME_ONBOARD 50 // C
Once in the state "On", the coco will be shoot after MONOCOCO_FIRE_COCO_AT
frames:
#define MONOCOCO_FIRE_COCO_AT MONOCOCO_BASE_TIME_ONBOARD/2
There are two kinds of monococos: type A (define MONOCOCO_TYPE_A
) and type B (comment MONOCOCO_TYPE_A
out).
- Type A: they use two cells which alternate continuously. Hidden during the idle state, flickering during appearing / disappearing.
- Type B: they use four cells: A "hidden" cell during idle, an "appear / disappear" cell during appearing / disappearing, and the alternative two animation cells during the on state.
So you need to provide 2 or 4 cells of animation in the spr_enems?
arrays depending on the type configured:
- Type A, two cells:
FRAME_A, FRAME_B
. - Type B, four cells:
FRAME_A, FRAME_B, APPEARING/DISAPPEARING, HIDDEN
.
In the spriteset, those are the animation cells:
(RIGHT) `FRAME_A`, `FRAME_B`, 0, 0, (LEFT) `FRAME_A`, `FRAME_B`, 0, 0, // All types
[(RIGHT) `APPEARING`, `HIDDEN`, 0, 0, (LEFT) `APPEARING`, `HIDDEN`], 0, 0, // Only type B
(only for side view)
Shooties and Punchies are additions to patrollers. Patroller enemies are types 1-3. You can turn a patroller into a shootie by adding 0x80 and into a punchie adding 0x40 (so a type 2 punchie would be 0x42, for example).
Shooties will shoot when the player enters its line of sight. Punchies will hit the player when it comes close.
Remember that the first three groups of cells in the spr_enems?
arrays are reserved for type 1, 2 and 3 patrollers. They will be used for related punchies and shooties. The cell groups are:
RIGHT_WALK_1, RIGHT_WALK_2, RIGHT_HITTING, RIGHT_DYING,
LEFT_WALK_1, LEFT_WALK_2, LEFT_HITTING, LEFT_DYING
When shooties shoot or punchies punch, the RIGHT_HITTING
or LEFT_HITTING
frame is displayed.
#define SHOOTIES_BASE_SPRID 32
If you want your shooties to look different from normal patrollers, add an offset here. For example, you can have three different shooties this way:
unsigned char spr_enems0 [] = {
e1rw1, e1rw2, e1rh, e1rd, e1lw1, e1lw2, e1lh, e1ld, // Patroller 1
e2rw1, e2rw2, e2rh, e2rd, e2lw1, e2lw2, e2lh, e2ld, // Patroller 2
e3rw1, e3rw2, e3rh, e3rd, e3lw1, e3lw2, e3lh, e3ld, // Patroller 3
pl1, pl2, 0, 0, pl1, pl2, 0, 0, // Platform
e1rw1, e1rw2, e1rh, e1rd, e1lw1, e1lw2, e1lh, e1ld, // Shooty 1
e2rw1, e2rw2, e2rh, e2rd, e2lw1, e2lw2, e2lh, e2ld, // Shooty 2
e3rw1, e3rw2, e3rh, e3rd, e3lw1, e3lw2, e3lh, e3ld, // Shooty 3
}
Using SHOOTIES_BASE_SPRID
with value 32 will make this work. A type 0x42 enemy is being loaded, the base index would be that of enemy type 2 plus 32: (2-1)*8 + 32 = 40, which points to e2rw1
.
#define SHOOTIES_SHOOT_OFFS_X 16
#define SHOOTIES_SHOOT_OFFS_Y -2
The coco will be shot from these offsets added to the enemy's origin of coordinates when facing right. The horizontal offset when facing left is autocalculated to match.
#define SHOOT_FREQ (pry+23>=en_y[gpit]&&pry<=en_y[gpit]+23&&((en_facing[gpit]&&en_x[gpit]>prx)||(en_facing[gpit]==0&&en_x[gpit]<prx))&&(rand8()&0x1f)==0)
The shootie will shoot when the expresion contained in SHOOT_FREQ
is true, so there's a lot of room for customization here. The above example makes the shootie shoot if the player is in front of him, roughly, with a random factor added to the mix.
#define PUNCHIES_BASE_SPRID 32
Works the same way SHOOTIES_BASE_SPRID
does.
#define PUNCHIES_PUNCH_OFFS_X 16
#define PUNCHIES_PUNCH_OFFS_Y -7
These define where the hitbox is located. The values are offsets from the enemy's origin of coordinates when facing right. The horizontal offset when facing left is autocalculated to match. The hitbox is 8x8.
#define PUNCH_FREQ (pry+23>=en_y[gpit]&&pry<=en_y[gpit]+23&&((en_facing[gpit]&&en_x[gpit]>prx)||(en_facing[gpit]==0&&en_x[gpit]<prx))&&DELTA(prx,en_x [gpit]+4)<16)
As with shooties' SHOOT_FREQ
, punchies will punch when the expression in PUNCH_FREQ
evaluates to true. The above example makes use of the DELTA
macro to determine if the player is close enough.
#define ENABLE_STEADY_SHOOTERS
Steady shooters (type 5) are just cannons. They shoot a coco in the direction they are facing. The direction is determined by the relation between (X1, Y1) and (X2, Y2). The attr
field in ponedor is used to define the number of seconds between shoots.
#define STEADY_SHOOTERS_BASE_SPRID 44
Steady shooters need 4 cells in your spr_enems?
arrays, one for each direction left, up, right, down, in that order. STEADY_SHOOTERS_BASE_SPRID
points to the sequence.
#define STEADY_SHOOTER_KILLABLE
This makes steady shooters killable. But beware: they are not counted as killable by eneexp3
's option gencounter
. If you want to take them in account you should compare pkilled
to KILLABLE_ENEMS_prefix + MAX_ENEMS_TYPE_5_prefix
.
#define ENABLE_COMPILED_ENEMS
Compiled enems (Type 0x14) follow a programmed pattern. A future tutorial or article will describe them in more depth.
#define COMPILED_ENEMS_SHOOT
If defined, compiled enems can shoot. Leave it out if they don't to save space.
#define COMPILED_ENEMS_BASE_SPRID 48
Compiled enems are rendered facing left or right, and need four cells per direction:
WALK_RIGHT_0
WALK_RIGHT_1
IDLE_RIGHT_0
IDLE_RIGHT_1
WALK_LEFT_0
WALK_LEFT_1
IDLE_LEFT_0
IDLE_LEFT_1
The constant COMPILED_ENEMS_BASE_SPRID
points to the first cell in the list.
Boioiongs are bouncing things which fall from (x1, y1) and move forward (in the direction x1->x2) and bouncing off the floor and walls until a timer is off. Think of bouncing barrels. You can make them active by default or off by default.
#define BOIOIONG_G 16
Defines gravity for the boioiong entities. To make them heavier than the player, use a higher number than PLAYER_G
.
#define BOIOIONG_ACTIVE_BY_DEFAULT
#define BOIOIONG_INITIAL_TIMER 200
Make boioiong entities active by default, and make them last BOIOING_INITIAL_TIMER
frames.
#define BOIOIONG_AUTO_RESPAWN
When the timer is 0, create a new entity at (x1, y1).
Cocos are activated when needed (which means that you don't have to define ENABLE_COCOS
yourself). But you should configure these values:
#define COCOS_MAX 4
The maximum number of cocos on screen. 4 is usually good, but if you notice that sometimes your enemies don't fire when they should, increase this value.
#define COCO_V 128
Velocity in 1/64ths of pixel per frame. That 128 means 2 pixels per frame.
#define COCO_COLLIDES
If defined, cocos collide with the background. If not, cocos will go through everything until they exit the game area.
#define COCO_PATTERN 0
#define COCO_PALETTE 0
Used to draw cocos.
#define COCO_FAIR_D 32
Fair distance - Cocos won't be shoot if the player's distance to the shooter is less than COCO_FAIR_D
. If you want this feature off leave it at 0.
#define ENABLE_ONLY_ONE_OBJECT
If defined, you can only carry one "collectible item" (hotspots type 1). Once you are carrying one, only an external factor can clear your inventory (and possibly increment a counter). This external factor can be injected code or the scripting system.
#define ONLY_ONE_OBJECT_FLAG 0
In this mode, if the player got an object, then pinv == 1
, 0 otherwise. If you define ONLY_ONE_OBJECT_FLAG
, then flags [ONLY_ONE_OBJECT_FLAG]
will be used instead of pinv.
You usually use pinv if you are implementing your extended game logic via code injection, or flags if you are using scripting.
Che Man uses ENABLE_ONLY_ONE_OBJECT
and scripting.
#define ENABLE_EASY_OBJECTS
To get a proper explanation on how Easy Objects work, check the README.md of Cheril the Writter
(only for side view)
The player can be given the ability to kick (while jumping) or punch (on floor) (separately).
#define PLAYER_PUNCHES // When on floor
#define PLAYER_PUNCH_OFFS_X 15
#define PLAYER_PUNCH_OFFS_Y -7
#define PLAYER_KICKS // While airborne
#define PLAYER_KICK_OFFS_X 12
#define PLAYER_KICK_OFFS_Y -3
The *_OFFS_?
constants define offsets for the hit box from the player's origin of coordinates when facing right. The correct horizontal offset when facing left is auto-calculated to match.
#define PLAYER_FROZEN_FRAMES 16
When the player lands a kick or puch, it (and the actor which was hit) will be fronzen for PLAYER_FROZEN_FRAMES
frames.
#define PLAYER_CAN_FIRE
Shooting can be heavily customized. For basic shooting:
#define PLAYER_CAN_FIRE_8_WAY
Leave it undefined for 4-way (top-down) or 2-way (side view) shooting. If defined, player can shoot in all 8 directions (top-down and side view) using the D-PAD alongisde B to shoot.
#define PLAYER_BULLET_SPEED 4
Bullets speed, on pixels per frame.
#define MAX_BULLETS 4
Max # of simultaneous bullets on screen. Slower speeds may require a bigger number than 4.
#define PLAYER_BULLET_Y_OFFSET 6
#define PLAYER_BULLET_X_OFFSET -4
Bullet offset from the player sprite. PLAYER_BULLET_X_OFFSET
is used only on top-down games. Here's an explanation on the values.
Shooting left or right
prx-8 prx prx+8
/ O------+··········pry
| | |
Y_OFFSET\ O----+| |O----+····pry + Y_OFFSET
| <- || || -> |
| || || |
+----+| |+----+
| |
+------+
Shooting up or down (top-down)
pry-4···O----+ UP
| |
pry·····| O-|----+
+----+ |
| |
| |
| |
| |
pry+12·····| O-|--+
+------+ |
| |
+----+ DOWN
|
prx - X_OFFSET
|
prx
|
prx + X_OFFSET
Appart from the basic configuration, there are some options available you may choose to activate or not:
#define PLAYER_BULLETS_MIN_KILLABLE 3
If defined, only enemies of type >= PLAYER_BULLETS_MIN_KILLABLE
can be killed.
#define BULLETS_DONT_KILL
Bullets hit on enemies, but don't kill them. They still affect enemies: for example, if ENEMS_RECOIL_ON_HIT
is defined, enemies will recoil without dying.
#define PLAYER_FIRE_RELOAD 16
If defined, player can't shoot again until PLAYER_FIRE_RELOAD
frames have passed since last shoot.
#define PLAYER_CHARGE_AND_FIRE
#define PLAYER_CHARGE_MIN 8
#define PLAYER_CHARGE_MAX 48
These three go together. If PLAYER_CHARGE_AND_FIRE
is defined, player has to hit B to charge and shoot when the button is released.
When this happens, pfiregauge
is incremented by 1 each frame while B is pressed while pfiregauge < PLAYER_CHARGE_MAX
. If B is released and pfiregauge >= PLAYER_CHARGE_MIN
, a bullet will be shoot.
#define PLAYER_BULLET_LIFE 16
#define PLAYER_BULLET_FLICKERS 8
If defined, bullets will only live for PLAYER_BUFFER_LIFE
frames. Note that you can use an expression instead of a constant value. That way, you can make some use of PLAYER_CHARGE_AND_FIRE
. For example:
#define PLAYER_BULLET_LIFE pfiregauge
Will bind the fire gauge and the bullets life directly.
If PLAYER_BULLET_FLICKERS
is defined, bullets will blink for PLAYER_BULLET_FLICKERS
frames before disappearing.
By default, there's no ammo limit. You can add one easily:
#define MAX_AMMO 99
#define AMMO_REFILL 50
If defined, the player can only shoot MAX_AMMO bullets. If the player gets a HOTSPOT_TYPE_AMMO
hotspot, AMMO_REFILL
bullets will be added.
By default, if you define an ammo limit, the player will start the game with such amount of bullets, but you can define a different value if you need:
#define INITIAL_AMMO 8
#define ACTIVATE_SCRIPTING
Includes the scripts, the interpreter, and enables the hooks. There's additional configuration:
#define CLEAR_FLAGS
If defined, the engine will zero all flags when initializing each level. You don't want this, for example, if you have several levels and need to carry values from one script to the next.
#define ENABLE_EXTERN_CODE
If defined, EXTERN n
in the script will make a call to do_extern_action (n)
. The function is defined in my/extern.h
. You can extend the script capabilities easily making this function react to the passed value.
#define ENABLE_FIRE_ZONE
Define ENABLE_FIRE_ZONE
if you use fire zones in your script.
#define ENABLE_INTERACTIVES
Remember interactives are created when entering the screen.
#define INTERACTIVES_MAX 4
The maximum number of interactives in a single screen. Use the lowest number possible!
#define FLAG_INVENTORY 0
Which flag will contain the item the player is carrying.
#define INTERACTIVES_ONLY_SPRITES
If you are only using interactives to show sprites (like, for example, Cheril the Writer), defininig INTERACTIVES_ONLY_SPRITES
will save tons of space.
#define PLAYER_TOP_DOWN
Define PLAYER_TOP_DOWN
to enable top-down mode. This overrides all side-view related settings.
#define TOP_OVER_SIDE
If defined, on diagonals, UP/DOWN has priority over LEFT/RIGHT.
The game will be a side-view platformer if PLAYER_TOP_DOWN
is commented out. Then you can configure which kind of platformer:
#define PLAYER_HAS_JUMP
//#define PLAYER_JUMP_TYPE_MK2
PLAYER_HAS_JUMP
binds the jumping action to the A button. If, additionally, PLAYER_JUMP_TYPE_MK2
is defined, alternate (less floaty) jump physics are used.
#define PLAYER_AUTO_JUMP
Jumping is not bound to the A button, but it's performed automaticly. Whenever the player lands on a platform or walkable floor, it will jump.
#define PLAYER_SWIMS
Very basic swimming engine. Player will naturally float up. Press A or DOWN to make it sink.
#define PLAYER_HAS_JETPAC
Basic Jet Pack. Player has to press A to thrust.
Note that you can activate more than one of those compiler directives. Then, you can choose the active engine for vertical movement using the vertical_engine_type
variable, which may take these values:
ENGINE_TYPE_JUMP
ENGINE_TYPE_JET_PAC
ENGINE_TYPE_SWIM
ENGINE_TYPE_AUTO_JUMP
This can help you change engines between levels or even mid-level.
#define PLAYER_STEPS_ON_ENEMS
//#define PLAYER_STEPS_STRICT
Jump on enemies to crush them. If PLAYER_STEPS_STRICT
is defined, pvy > PLAYER_VY_MIN
to crush the enemies.
#define PLAYER_SAFE_LANDING
If defined, if you step on a patroller which is going up it will change directions and go down.
#define PLAYER_STEPS_MIN_KILLABLE 0xff
If defined, only kill enemies with id >= PLAYER_STEPS_MIN_KILLABLE
. You can use 0xff to make all enemies invulnerable.
Determine where to display info. Note that if you don't want a certain piece of info to appear on screen, you should comment out the #define
:
//#define LIFE_X 7 //
//#define LIFE_Y 3 // Life gauge counter character coordinates
//#define OBJECTS_X 18 //
//#define OBJECTS_Y 3 // Objects counter character coordinates
#define OBJECTS_REMAINING // Show # remaining instead of got
//#define KEYS_X 28 //
//#define KEYS_Y 3 // Keys counter character coordinates
//#define KILLED_X 16 //
//#define KILLED_Y 2 // Kills counter character coordinates
//#define AMMO_X 8 //
//#define AMMO_Y 2 // Ammo counter character coordinates
//#define HS_INV_X 136 //
//#define HS_INV_Y 11 // Object you are carrying
//#define TIMER_X 0 //
//#define TIMER_Y 5 // Current timer value
// Text
//#define LINE_OF_TEXT 26 // If defined, scripts can show text @ Y = #
//#define LINE_OF_TEXT_X 1 // X coordinate.
Determine how the player moves. There's a lot to tweak here.
Vertical axis has quite a lot of values:
#define PLAYER_VY_FALLING_MAX 256 // Max. velocity when falling
#define PLAYER_VY_FALLING_MIN 64 // Use for animating if you need
#define PLAYER_VY_SINKING 2
#define PLAYER_G 16 // Gravity
#define PLAYER_VY_JUMP_INITIAL 64
#define PLAYER_VY_JUMP_MAX 192 // Max. velocity when jumping
#define PLAYER_AY_JUMP 12 // Jumpin acceleration
#define PLAYER_AY_JETPAC 32 // Jetpac increment
#define PLAYER_VY_JETPAC_MAX 256 // Max jetpac vertical speed
#define PLAYER_AY_SWIM 8 // Swimming acceleration.
#define PLAYER_VY_SWIM_MAX 64 // Swimming max. speed
#define PLAYER_VY_LADDERS 96
#define PLAYER_AY_FLOAT 16
#define PLAYER_VY_FLOAT_MAX 256
#define PLAYER_AY_UNTHRUST 8 // Used in the Autojump engine.
If you are using PLAYER_JUMP_TYPE_MK2
, these constants are used instead of PLAYER_?Y_JUMP_*
.
// IV.1.b MK2 style jump (overrides PLAYER_?Y_JUMP_* defined before!)
// (Used if PLAYER_JUMP_TYPE_MK2 is defined)
#define PLAYER_G_MK2_JUMPING 4
#define PLAYER_VY_MK2_JUMP_INITIAL 208
#define PLAYER_VY_MK2_JUMP_RELEASE 96
#define PLAYER_VY_MK2_JUMP_A_STEPS 16
And for the horizontal movement (and vertical, in top-down style games)
// IV.2. Horizontal (side view) or general (top view) movement.
#define PLAYER_VX_MAX 128 // Max. horizontal speed
#define PLAYER_VX_CONVEYORS 64
#define PLAYER_AX 16 // Horizontal acceleration
#define PLAYER_AX_ICE 4
#define PLAYER_RX 16 // Horizontal friction
#define PLAYER_RX_ICE 2
#define PLAYER_VX_MIN (PLAYER_AX << 1)
#define PLAYER_V_REBOUND 224
Maps indexes in the spr_player
array to player states. You have to tweak these values to match your metasprite array and animations in my/player_frame_selector.h
.
#ifdef PLAYER_TOP_DOWN
// Cell definitions for top-down view
#define CELL_FACING_RIGHT 0
#define CELL_FACING_LEFT 6
#define CELL_FACING_UP 18
#define CELL_FACING_DOWN 12
#define CELL_IDLE 0
#define CELL_WALK_CYCLE 1
#define CELL_PUSHING 5
#define CELL_USE 24
#else
// Cell definitions for side view
#define CELL_FACING_RIGHT 0
#define CELL_FACING_LEFT 8
#define CELL_IDLE 0
#define CELL_WALK_INIT 1
#define CELL_WALK_CYCLE 1
#define CELL_AIRBORNE 5
#define CELL_ASCENDING 5
#define CELL_DESCENDING 6
#define CELL_SWIM_CYCLE 6
#define CELL_USE 6
#define CELL_PUNCHING 8
#define CELL_KICKING 9
#define CELL_CLIMB_CYCLE 20
#define CELL_CLIMB_HALF 29
#endif
You may want to alter the values if you use your own set of sounds.
// SFX
#define SFX_START 0
#define SFX_TILE 1
#define SFX_OBJECT 2
#define SFX_USE 3
#define SFX_PHIT 4
#define SFX_DUMMY1 5
#define SFX_ENHIT 6
#define SFX_DUMMY2 7
#define SFX_JUMP 8
#define SFX_BULLET 9
#define SFX_COCO 10
#define SFX_SPRING 11
#define SFX_COUNT 12
#define SFX_BREAKH 13
#define SFX_HITTER 14
#define SFX_STEPON 15
#define SFX_FLOAT 16
#define SFX_BREAKB 17