coordinates: rework map rendering

This also adds interactive player movement.

I am much more satisfied with the coordinate space math in this
commit compared to the previous commit.
This commit is contained in:
Zack Buhman 2023-07-28 05:46:36 +00:00
parent 9645b4b414
commit e6cae1c242
8 changed files with 282 additions and 147 deletions

View File

@ -1,6 +1,6 @@
CFLAGS = -Isaturn CFLAGS = -Isaturn
OPT ?= -Og OPT ?= -O3
LIB = ./saturn LIB = ./saturn
GEN_PYTHON_SOURCE = $(wildcard tools/source/*.py) $(wildcard tools/generate/*.py) GEN_PYTHON_SOURCE = $(wildcard tools/source/*.py) $(wildcard tools/generate/*.py)
@ -84,7 +84,7 @@ res/%.bst: pokered/%.bst
%.o: | $(GFX_TILESETS) %.o: | $(GFX_TILESETS)
main.elf: $(OBJ) main.elf: $(OBJ) $(LIBGCC)
clean: clean-sh clean: clean-sh
clean-sh: clean-sh:

66
actor.hpp Normal file
View File

@ -0,0 +1,66 @@
#pragma once
#include "coordinates.hpp"
struct actor_t
{
enum direction {
up,
down,
left,
right,
};
world_t world;
enum direction facing;
bool moving;
uint8_t frame;
constexpr inline offset_t offset()
{
switch (facing) {
case left:
return {-frame, 0};
case right:
return {frame, 0};
case up:
return {0, -frame};
case down:
return {0, frame};
default:
return {0, 0};
}
}
// call tick() before move()
constexpr inline void tick()
{
if (!moving) return;
if (++frame == 16) {
moving = false;
frame = 0;
switch (facing) {
case left: world.x--; break;
case right: world.x++; break;
case up: world.y--; break;
case down: world.y++; break;
}
}
}
constexpr inline void move(enum direction dir)
{
/*
m 1 . 2 . 3 . 4 .
| - | - | - | - |
*/
if (moving) return;
frame = 0;
facing = dir;
moving = true;
}
};

67
coordinates.hpp Normal file
View File

@ -0,0 +1,67 @@
#pragma once
#include <stdint.h>
namespace screen_offset
{
// the origin is at this offset in screen space
constexpr int32_t x = 4;
constexpr int32_t y = 4;
}
struct offset_t { // in pixels
int32_t x;
int32_t y;
};
struct block_t {
int32_t x;
int32_t y;
};
struct world_t;
struct screen_t {
int32_t x;
int32_t y;
constexpr inline world_t to_world(const world_t& origin) const;
};
struct world_t {
int32_t x;
int32_t y;
constexpr inline screen_t to_screen(const world_t& origin) const;
constexpr inline block_t to_block() const;
};
// screen_t
constexpr inline world_t screen_t::to_world(const world_t& origin) const
{
return {
(origin.x - screen_offset::x) + x,
(origin.y - screen_offset::y) + y,
};
}
// world_t
constexpr inline screen_t world_t::to_screen(const world_t& origin) const
{
return {
-origin.x + screen_offset::x + x,
-origin.y + screen_offset::y + y,
};
}
constexpr inline block_t world_t::to_block() const
{
// division function must round towards negative infinity
// right shift provides that property
return {
x >> 1,
y >> 1,
};
}

View File

@ -29,8 +29,8 @@ void digital_callback(uint8_t fsm_state, uint8_t data);
extern input_t input; extern input_t input;
constexpr int input_arr = 10; constexpr int input_arr = 1;
constexpr int input_das = 20; constexpr int input_das = 1;
constexpr int input_debounce = 2; constexpr int input_debounce = 2;
static constexpr inline int32_t static constexpr inline int32_t
@ -52,6 +52,8 @@ input_flopped(count_flop_t& button)
} }
struct event { struct event {
static inline bool cursor_left() { return input_flopped(input.a) == 1; } static inline bool cursor_left() { return input_flopped(input.left ) >= 1; }
static inline bool cursor_right() { return input_flopped(input.b) == 1; } static inline bool cursor_right() { return input_flopped(input.right) >= 1; }
static inline bool cursor_up() { return input_flopped(input.up ) >= 1; }
static inline bool cursor_down() { return input_flopped(input.down ) >= 1; }
}; };

197
main.cpp
View File

@ -16,6 +16,10 @@
#include "gen/sprites.hpp" #include "gen/sprites.hpp"
#include "map_objects.hpp" #include "map_objects.hpp"
#include "coordinates.hpp"
#include "render_map.hpp"
#include "actor.hpp"
constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b) constexpr inline uint16_t rgb15(int32_t r, int32_t g, int32_t b)
{ {
return ((b & 31) << 10) | ((g & 31) << 5) | ((r & 31) << 0); return ((b & 31) << 10) | ((g & 31) << 5) | ((r & 31) << 0);
@ -75,28 +79,6 @@ uint32_t cell_data(const start_size_t& buf, const uint32_t top)
return base_address; return base_address;
} }
constexpr inline void render_block(const uint32_t base_pattern,
const tileset_t& tileset,
const int32_t map_x,
const int32_t map_y,
const uint8_t block)
{
for (int32_t block_y = 0; block_y < 4; block_y++) {
for (int32_t block_x = 0; block_x < 4; block_x++) {
const int32_t block_ix = 4 * block_y + block_x;
const uint8_t tile_ix = tileset.blockset.start[block * 4 * 4 + block_ix];
const uint32_t cell_y = map_y * 4 + block_y;
const uint32_t cell_x = map_x * 4 + block_x;
// assumes NBG0 map plane_a is at offset 0
vdp2.vram.u16[64 * (cell_y % 64) + (cell_x % 64)] = (base_pattern & 0xfff) + tile_ix;
//vdp2.vram.u32[64 * cell_y + cell_x] = base_pattern + tile_ix;
}
}
}
constexpr int32_t last_map = map_t::wardens_house;
struct draw_t { struct draw_t {
struct { struct {
uint16_t tilesets[tileset_t::count]; // div 32 uint16_t tilesets[tileset_t::count]; // div 32
@ -104,17 +86,10 @@ struct draw_t {
} base_pattern; } base_pattern;
}; };
struct player_t {
struct {
int32_t x;
int32_t y;
};
};
struct state_t { struct state_t {
enum map_t::map map; enum map_t::map map;
draw_t draw; draw_t draw;
player_t player; actor_t player;
}; };
static state_t state = { map_t::pallet_town, 0 }; static state_t state = { map_t::pallet_town, 0 };
@ -157,17 +132,10 @@ void load_vram()
vdp1_top = load_sprite(vdp1_top, static_cast<enum spritesheet_t::spritesheet>(i)); vdp1_top = load_sprite(vdp1_top, static_cast<enum spritesheet_t::spritesheet>(i));
} }
// screen space void render_sprite(const uint32_t ix, const uint32_t sprite_id, const screen_t& screen, const offset_t& offset)
constexpr int32_t origin_px_x = ((320 - 160) / 2);
constexpr int32_t origin_px_y = ((240 - 144) / 2);
constexpr int32_t origin_cell_x = origin_px_x / 8;
constexpr int32_t origin_cell_y = origin_px_y / 8;
void render_sprites()
{ {
uint32_t ix = 2;
constexpr uint32_t color_address = 0; constexpr uint32_t color_address = 0;
const uint32_t character_address = (state.draw.base_pattern.spritesheets[spritesheet_t::oak] * 128) / 8; const uint32_t character_address = (state.draw.base_pattern.spritesheets[sprite_id] * 128) / 8;
vdp1.vram.cmd[ix].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__NORMAL_SPRITE; vdp1.vram.cmd[ix].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__NORMAL_SPRITE;
vdp1.vram.cmd[ix].LINK = 0; vdp1.vram.cmd[ix].LINK = 0;
@ -181,11 +149,28 @@ void render_sprites()
vdp1.vram.cmd[ix].COLR = color_address; // non-palettized (rgb15) color data vdp1.vram.cmd[ix].COLR = color_address; // non-palettized (rgb15) color data
vdp1.vram.cmd[ix].SRCA = character_address; vdp1.vram.cmd[ix].SRCA = character_address;
vdp1.vram.cmd[ix].SIZE = SIZE__X(16) | SIZE__Y(16); vdp1.vram.cmd[ix].SIZE = SIZE__X(16) | SIZE__Y(16);
vdp1.vram.cmd[ix].XA = origin_px_x + 4 * 16; vdp1.vram.cmd[ix].XA = (cell_offset::x * 8) + screen.x * 16 - offset.x;
vdp1.vram.cmd[ix].YA = origin_px_y + 4 * 16 - 4; vdp1.vram.cmd[ix].YA = (cell_offset::y * 8) + screen.y * 16 - 4 - offset.y;
}
void render_sprites(const offset_t& offset)
{
uint32_t ix = 2;
render_sprite(ix, spritesheet_t::red, {4, 4}, {0, 0});
ix++; ix++;
const object_t& obj = map_objects[state.map];
for (uint32_t i = 0; i < obj.object_length; i++) {
const object_event_t& event = obj.object_events[i];
const world_t world = { event.position.x, event.position.y };
render_sprite(ix,
event.sprite_id,
world.to_screen(state.player.world),
offset);
ix++;
}
constexpr uint16_t top_x = 80 - 1; constexpr uint16_t top_x = 80 - 1;
constexpr uint16_t top_y = 48 - 1; constexpr uint16_t top_y = 48 - 1;
constexpr uint16_t bot_x = 239 + 1; constexpr uint16_t bot_x = 239 + 1;
@ -216,116 +201,54 @@ void render_sprites()
vdp1.vram.cmd[ix].CTRL = CTRL__END; vdp1.vram.cmd[ix].CTRL = CTRL__END;
} }
static uint16_t scroll = 0;
/*
in block units:
world screen
-----------|---------
player 0,0 | map 4,4
scrolled 16,16
player 1,1 |
world | screen
--------------------------|---------
top left = player x - 5 | (add 4)
top right = player x - 5 | (add 4)
----
screen scroll
0,0 -> screen 0,0
1,1 -> screen 16,16
*/
// there are 16 pixels per block
static inline uint8_t get_block(const map_t& map, int32_t block_x, int32_t block_y)
{
const uint8_t border_block = map_objects[state.map].border_block;
const bool x_lt = block_x < static_cast<int32_t>(map.width);
const bool y_lt = block_y < static_cast<int32_t>(map.height);
const bool x_gt = block_x >= 0;
const bool y_gt = block_y >= 0;
const bool inside_map = x_lt && y_lt && x_gt && y_gt;
const bool north = x_lt && x_gt && !(y_gt);
const bool south = x_lt && x_gt && !(y_lt);
const bool west = y_lt && y_gt && !(x_gt);
const bool east = y_lt && y_gt && !(x_lt);
#define _has(dir) (map.connections[map_t::connection_t::dir].map != map_t::unconnected)
#define _get(dir) (maps[map.connections[map_t::connection_t::dir].map])
#define _offset(dir) (map.connections[map_t::connection_t::dir].offset)
if (inside_map) {
return map.blocks.start[map.width * block_y + block_x];
} else if (north && _has(north)) {
const map_t& north_map = _get(north);
const uint32_t north_y = north_map.height + block_y;
const uint32_t north_x = block_x - _offset(north);
return get_block(north_map, north_x, north_y);
} else {
return border_block;
}
#undef _offset
#undef _get
#undef _has
}
void render_map() void render_map()
{ {
state.player.x = 8 * 16;
if (++scroll > 1) {
scroll = 0;
//state.player.x += 1;
state.player.y -= 1;
}
vdp2.reg.SCXIN0 = state.player.x - 16;
vdp2.reg.SCYIN0 = state.player.y - 16;
/*
vdp2.reg.WPSX0 = 80 << 1;
vdp2.reg.WPSY0 = 48;
vdp2.reg.WPEX0 = 239 << 1;
vdp2.reg.WPEY0 = 191;
vdp2.reg.WCTLA = WCTLA__N0W0E | WCTLA__N0W0A__OUTSIDE;
*/
const map_t& map = maps[state.map]; const map_t& map = maps[state.map];
const uint32_t base_pattern = state.draw.base_pattern.tilesets[map.tileset]; const uint32_t base_pattern = state.draw.base_pattern.tilesets[map.tileset];
vdp2.reg.PNCN0 = PNCN0__N0PNB__1WORD | PNCN0__N0CNSM | PNCN0__N0SCN((base_pattern >> 10) & 0x1f); vdp2.reg.PNCN0 = PNCN0__N0PNB__1WORD | PNCN0__N0CNSM | PNCN0__N0SCN((base_pattern >> 10) & 0x1f);
int32_t origin_x = state.player.x / 32; for (int32_t y = (0 - 1); y < (9 + 1); y++) {
int32_t origin_y = state.player.y / 32; for (int32_t x = (0 - 1); x < (10 + 1); x++) {
render_screen(base_pattern,
fill<uint32_t>(vdp2.vram.u32, 0, 64 * 64 * 2); map,
state.player.world,
for (int32_t y = origin_y - 3; y <= (origin_y + 3); y++) { {x, y}
for (int32_t x = origin_x - 3; x <= (origin_x + 3); x++) { );
const uint8_t block = get_block(map, x, y);
render_block(base_pattern,
tilesets[map.tileset],
x + 4,
y + 3,
block);
} }
} }
vdp2.reg.BGON = BGON__N0ON | BGON__N0TPON; vdp2.reg.BGON = BGON__N0ON | BGON__N0TPON;
} }
void update()
{
state.player.tick();
if (event::cursor_left()) {
state.player.move(actor_t::left);
//state.player.world.x--;
} else if (event::cursor_right()) {
state.player.move(actor_t::right);
//state.player.world.x++;
} else if (event::cursor_up()) {
state.player.move(actor_t::up);
//state.player.world.y--;
} else if (event::cursor_down()) {
state.player.move(actor_t::down);
//state.player.world.y++;
}
}
void render() void render()
{ {
const offset_t offset = state.player.offset();
render_sprites(offset);
vdp2.reg.SCXIN0 = offset.x;
vdp2.reg.SCYIN0 = offset.y;
render_map(); render_map();
render_sprites();
} }
extern "C" extern "C"
@ -339,6 +262,7 @@ void v_blank_in_int()
sh2.reg.FRC.L = 0; sh2.reg.FRC.L = 0;
sh2.reg.FTCSR = 0; // clear flags sh2.reg.FTCSR = 0; // clear flags
update();
render(); render();
// wait at least 300us, as specified in the SMPC manual. // wait at least 300us, as specified in the SMPC manual.
@ -476,7 +400,6 @@ void init_vdp2()
void main() void main()
{ {
state.map = map_t::pallet_town; state.map = map_t::pallet_town;
state.player.y = 16 * 8;
load_vram(); load_vram();

View File

@ -31,7 +31,7 @@ struct object_event_t {
pokemon, pokemon,
}; };
enum struct movement { enum struct movement { // or 2-byte boulder movement?
stay, stay,
walk, walk,
}; };
@ -50,7 +50,7 @@ struct object_event_t {
enum type type; enum type type;
position_t position; position_t position;
enum spritesheet_t::spritesheet sprite_id; // fixme enum spritesheet_t::spritesheet sprite_id;
enum movement movement; enum movement movement;
enum range_or_direction range_or_direction; enum range_or_direction range_or_direction;
uint8_t text_id; // fixme uint8_t text_id; // fixme

75
render_map.hpp Normal file
View File

@ -0,0 +1,75 @@
#pragma once
#include "coordinates.hpp"
#include "gen/maps.hpp"
#include "map_objects.hpp"
#include "vdp2.h"
static inline uint8_t get_block(const map_t& map, block_t block)
{
const uint8_t border_block = map_objects[map.id].border_block;
const bool x_lt = block.x < static_cast<int32_t>(map.width);
const bool y_lt = block.y < static_cast<int32_t>(map.height);
const bool x_gt = block.x >= 0;
const bool y_gt = block.y >= 0;
const bool inside_map = x_lt && y_lt && x_gt && y_gt;
const bool north = x_lt && x_gt && !(y_gt);
const bool south = x_lt && x_gt && !(y_lt);
const bool west = y_lt && y_gt && !(x_gt);
const bool east = y_lt && y_gt && !(x_lt);
#define _has(_dir_) (map.connections[map_t::connection_t::_dir_].map != map_t::unconnected)
#define _get(_dir_) (maps[map.connections[map_t::connection_t::_dir_].map])
#define _offset(_dir_) (map.connections[map_t::connection_t::_dir_].offset)
if (inside_map) {
return map.blocks.start[map.width * block.y + block.x];
} else if (north && _has(north)) {
map_t north_map = _get(north);
return get_block(north_map, {
.x = block.x - _offset(north),
.y = static_cast<int32_t>(north_map.height + block.y),
});
} else {
return border_block;
}
#undef _offset
#undef _get
#undef _has
}
namespace cell_offset
{
int32_t x = (320 - 160) / (2 * 8);
int32_t y = (240 - 144) / (2 * 8);
}
static inline void render_screen(const uint32_t base_pattern,
const map_t& map,
const world_t& origin,
const screen_t& screen)
{
const world_t world = screen.to_world(origin);
const block_t block = world.to_block();
const uint8_t block_ix = get_block(map, block);
const uint8_t * block_start = &(tilesets[map.tileset].blockset.start)[block_ix * 4 * 4];
const int32_t quadrant_x = 2 * (world.x & 1);
const int32_t quadrant_y = 2 * (world.y & 1);
for (int32_t tile_y = quadrant_y; tile_y < quadrant_y + 2; tile_y++) {
const int32_t block_row = 4 * tile_y;
for (int32_t tile_x = quadrant_x; tile_x < quadrant_x + 2; tile_x++) {
const uint8_t tile_ix = block_start[block_row + tile_x];
const uint32_t cell_y = cell_offset::y + screen.y * 2 + tile_y - quadrant_y;
const uint32_t cell_x = cell_offset::x + screen.x * 2 + tile_x - quadrant_x;
vdp2.vram.u16[64 * (cell_y % 64) + (cell_x % 64)] = (base_pattern & 0xfff) + tile_ix;
}
}
}

View File

@ -97,10 +97,11 @@ def struct_map_t():
*struct_connection_t(), *struct_connection_t(),
"", "",
"start_size_t blocks;", "start_size_t blocks;",
"enum tileset_t::tileset tileset;",
"uint32_t width;", "uint32_t width;",
"uint32_t height;", "uint32_t height;",
"connection_t connections[4];", "connection_t connections[4];",
"enum tileset_t::tileset tileset;",
"enum map id;",
"", "",
f"static constexpr int32_t count = {len(_sorted_map_headers)};", f"static constexpr int32_t count = {len(_sorted_map_headers)};",
"};", "};",
@ -172,12 +173,13 @@ def map(map_header):
".blocks = {", ".blocks = {",
*start_size_value(block_path), *start_size_value(block_path),
"},", "},",
f".tileset = tileset_t::{map_header.tileset.lower()},",
f".width = {map_constant.width},", f".width = {map_constant.width},",
f".height = {map_constant.height},", f".height = {map_constant.height},",
".connections = {", ".connections = {",
*connections(map_header), *connections(map_header),
"},", "},",
f".tileset = tileset_t::{map_header.tileset.lower()},",
f".id = map_t::{map_header.name2.lower()},",
"},", "},",
] ]