initial CPC version (with some Azur refactoring)

This commit is contained in:
Lephenixnoir 2022-11-19 17:38:40 +01:00
commit f7a027d6bf
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
32 changed files with 1471 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Build files
/build-fx
/build-cg
/*.g1a
/*.g3a
# Python bytecode
__pycache__/
# Common IDE files
*.sublime-project
*.sublime-workspace
.vscode

49
CMakeLists.txt Normal file
View File

@ -0,0 +1,49 @@
cmake_minimum_required(VERSION 3.15)
project(Afterburner VERSION 1.0 LANGUAGES CXX C ASM)
include(GenerateG3A)
include(Fxconv)
find_package(Azur 0.1 REQUIRED)
find_package(Gint 2.8 REQUIRED)
find_package(LibProf 2.1 REQUIRED)
set(SOURCES
src/camera.cpp
src/image.cpp
src/main.cpp
src/horizon.cpp
src/horizon.s
src/world.cpp
)
set(ASSETS
assets-cg/plane.png
assets-cg/square.png
assets-cg/dot.png
assets-cg/tree_1.png
assets-cg/tree_2.png
assets-cg/tree_3.png
assets-cg/mountain_1.png
assets-cg/rock_1.png
assets-cg/rock_2.png
assets-cg/base_1.png
assets-cg/base_2.png
assets-cg/base_3.png
assets-cg/minimap.png
assets-cg/expl_1.png
assets-cg/expl_2.png
assets-cg/expl_3.png
assets-cg/expl_4.png
assets-cg/font.png
)
fxconv_declare_assets(${ASSETS} WITH_METADATA)
add_executable(afterburner ${SOURCES} ${ASSETS})
target_compile_options(afterburner PRIVATE -Wall -Wextra -Os -std=c++20)
target_link_options(afterburner PRIVATE -Wl,-Map=map -Wl,--print-memory-usage)
target_link_libraries(afterburner Azur::Azur -lnum LibProf::LibProf Gint::Gint)
generate_g3a(TARGET afterburner OUTPUT "AfterBur.g3a"
NAME "Afterburner" ICONS assets-cg/icon-uns.png assets-cg/icon-sel.png)

7
TODO Normal file
View File

@ -0,0 +1,7 @@
TODO:
* Title screen
* Multiple games
* Limit number of bombs
* Fix map boundary tests
* More interesting map generation
* Expand gameplay: missions, enemies, etc

BIN
assets-cg/base_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

BIN
assets-cg/base_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets-cg/base_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets-cg/dot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

BIN
assets-cg/expl_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets-cg/expl_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets-cg/expl_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets-cg/expl_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets-cg/font.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,7 @@
*.png:
type: bopti-image
name_regex: (.*)\.png img_\1
profile: p8_rgb565a
minimap.png:
profile: rgb565a

BIN
assets-cg/icon-sel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets-cg/icon-uns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets-cg/icon.xcf Normal file

Binary file not shown.

BIN
assets-cg/minimap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

BIN
assets-cg/mountain_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets-cg/plane.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
assets-cg/rock_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets-cg/rock_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

BIN
assets-cg/square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

BIN
assets-cg/tree_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets-cg/tree_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets-cg/tree_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

188
src/afterburner.h Normal file
View File

@ -0,0 +1,188 @@
#ifndef AFTERBURNER_H
#define AFTERBURNER_H
#include <gint/display.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <num/num.h>
#include <num/vec.h>
#include <libprof.h>
using namespace libnum;
//---
// Horizon shader
//---
/* Render the horizon line with a palette. The horizon line passes through
(x,y) and has the specified angle. This is an Azur shader, the function only
queues a command and delays the rendering to azrp_update(). */
void ab_horizon(int x, int y, float alpha, uint16_t *palette);
/* Configuration call, to be performed at every scale update */
void ab_horizon_configure(void);
//---
// Image utilities
//---
/* Allocate a new image from a rotation + upscaling of a square image img.
The source image must be RGB565A, P8_RGB565A or P4_RGB565A. */
bopti_image_t *image_rotate_scale_square
(bopti_image_t const *img, float alpha, num gamma);
/* Same but optimized for P8_RGB565A. *anchor_x and *anchor_y should be set to
the coordinates of the "anchor" within the source image, and are updated to
reflect the coordinates of that same spot in the rotated image. */
bopti_image_t *image_rotate_scale_square_p8_rgb565a
(bopti_image_t const *img,
float alpha, num gamma,
int *anchor_x, int *anchor_y);
/* An image with all upscaled variations from 0.125x to 2x. */
class DynamicImage
{
public:
DynamicImage(bopti_image_t const *source, float m_alpha, prof_t *prof_ctx,
int anchor_x, int anchor_y);
~DynamicImage();
/* Get the closest scaled version, generating it on demand. */
bopti_image_t *getAtScale(num scale, int *anchor_x, int *anchor_y);
private:
bopti_image_t *m_images[16];
int m_anchor_x, m_anchor_y;
int8_t m_ax[16], m_ay[16];
bopti_image_t const *m_source;
float m_alpha;
prof_t *m_prof;
};
//---
// 3D transforms and projections
//---
struct plane
{
plane() {
memset(this, 0, sizeof *this);
}
/* Position in space */
vec3 pos;
/* Euler angles with aviation terminology */
float roll, pitch, yaw;
/* Air speed */
num air_speed;
// The following are automatically set by plane_update()
/* Sine and cosine of every angle */
num cos_r, sin_r, cos_p, sin_p, cos_y, sin_y;
/* Vector looking forward and to the right */
vec3 forward, right;
/* Vector looking forward but parallel to the ground */
vec3 forward_ground;
};
struct mat3
{
num x11, x12, x13;
num x21, x22, x23;
num x31, x32, x33;
};
mat3 operator * (mat3 const &A, mat3 const &B);
vec3 operator * (mat3 const &M, vec3 const &u);
num distance2(vec3 u, vec3 v);
num64 distance2_64(vec3 u, vec3 v);
/* Update computationally-derived parameters of a plane */
void plane_update(struct plane *plane);
/* Pixels per world unit (primitive control for FOV) */
#define SCREEN_SCALING 200
/* Minimum and maximum distance of visible objects */
#define NEAR_CLIP_PLANE 8
#define FAR_CLIP_PLANE 50
#define SIDE_CLIP_PLANE 25
/* Margin around the screen where a center point is considered visible */
#define SCREEN_MARGIN 20
/* A 3D point with a handle to some sprite/information/etc. */
struct ref_point
{
/* Position of the point in 3D space (evolves during transformations) */
vec3 pos;
/* Unique identifier (used to identify point properties in world map) */
int id;
};
void transform_world2camera(struct plane const *plane, vec3 camera_pos,
struct ref_point *points, int size);
void transform_camera2screen(struct ref_point *points, int size);
//---
// World map
//---
/* Needs to be larger than FAR_CLIP_PLANE for culling optimisations to work */
#define CHUNK_SIZE 64
struct chunk
{
struct ref_point *points;
int count;
};
struct world
{
int w, h;
int chunks_x, chunks_y;
struct chunk *chunks;
};
/* Make a world structure using the specified set of points. The points are
split into chunks, which allows faster lookup later on. The points are all
copied, to the original array is safe to free after this call. */
struct world *world_make(int w, int h, struct ref_point *points, int count);
/* Destroy a world structure and its copied points. */
void world_destroy(struct world *w);
/* Select the points in the world that are "close" to the specified anchor.
These are points from the anchor's chunk and up to 8 neighboring chunks. The
size of the array is returned in *size. free() the array after use.
This function rotates chunks, so that for instance if position.x is 0.5 and
forward.x < 0 (ie. we look at a region of space with x<0), chunks from the
other side of the map are rotated in to provide a wrap-around effect.
extra_size entries are left uninitialized at the end of the array so that
any dynamic sprites can be added. *size does *not* count extra_size. */
struct ref_point *world_select(struct world const *w, vec3 position,
vec3 forward, int *size, int extra_size);
//---
// Game mechanics
//---
/* Distance forward where bombs drop */
#define BOMB_DISTANCE 15
struct objective
{
vec3 pos;
bool destroyed;
int type;
};
struct explosion
{
vec3 pos;
num time; /* if negative, no explosion */
};
#endif /* AFTERBURNER_H */

112
src/camera.cpp Normal file
View File

@ -0,0 +1,112 @@
#define __BSD_VISIBLE 1
#include "afterburner.h"
#include <azur/gint/render.h>
#include <math.h>
mat3 operator *(mat3 const &A, mat3 const &B)
{
mat3 C;
C.x11 = A.x11 * B.x11 + A.x12 * B.x21 + A.x13 * B.x31;
C.x12 = A.x11 * B.x12 + A.x12 * B.x22 + A.x13 * B.x32;
C.x13 = A.x11 * B.x13 + A.x12 * B.x23 + A.x13 * B.x33;
C.x21 = A.x21 * B.x11 + A.x22 * B.x21 + A.x23 * B.x31;
C.x22 = A.x21 * B.x12 + A.x22 * B.x22 + A.x23 * B.x32;
C.x23 = A.x21 * B.x13 + A.x22 * B.x23 + A.x23 * B.x33;
C.x31 = A.x31 * B.x11 + A.x32 * B.x21 + A.x33 * B.x31;
C.x32 = A.x31 * B.x12 + A.x32 * B.x22 + A.x33 * B.x32;
C.x33 = A.x31 * B.x13 + A.x32 * B.x23 + A.x33 * B.x33;
return C;
}
vec3 operator * (mat3 const &M, vec3 const &u)
{
vec3 v;
v.x = M.x11 * u.x + M.x12 * u.y + M.x13 * u.z;
v.y = M.x21 * u.x + M.x22 * u.y + M.x23 * u.z;
v.z = M.x31 * u.x + M.x32 * u.y + M.x33 * u.z;
return v;
}
num distance2(vec3 u, vec3 v)
{
u -= v;
return u.x * u.x + u.y * u.y + u.z * u.z;
}
num64 distance2_64(vec3 u, vec3 v)
{
u -= v;
return num32::dmul(u.x,u.x) + num32::dmul(u.y,u.y) + num32::dmul(u.z,u.z);
}
void plane_update(struct plane *plane)
{
float c, s;
sincosf(plane->roll, &s, &c);
plane->sin_r = s;
plane->cos_r = c;
sincosf(plane->pitch, &s, &c);
plane->sin_p = s;
plane->cos_p = c;
sincosf(plane->yaw, &s, &c);
plane->sin_y = s;
plane->cos_y = c;
plane->forward = vec3(
-plane->sin_y * plane->cos_p,
plane->cos_y * plane->cos_p,
plane->sin_p);
plane->forward_ground = vec3(-plane->sin_y, plane->cos_y, 0);
plane->right = vec3(plane->cos_y, plane->sin_y, 0);
}
void transform_world2camera(struct plane const *plane, vec3 camera_pos,
struct ref_point *points, int size)
{
num cos_r = plane->cos_r;
num sin_r = plane->sin_r;
num cos_p = plane->cos_p;
num sin_p = plane->sin_p;
num cos_y = plane->cos_y;
num sin_y = plane->sin_y;
mat3 m_roll = {
cos_r, 0, sin_r,
0, 1, 0,
-sin_r, 0, cos_r,
};
mat3 m_pitch = {
1, 0, 0,
0, cos_p, +sin_p,
0, -sin_p, cos_p,
};
mat3 m_yaw = {
cos_y, +sin_y, 0,
-sin_y, cos_y, 0,
0, 0, 1,
};
mat3 M = m_roll * (m_pitch * m_yaw);
for(int i = 0; i < size; i++)
points[i].pos = M * (points[i].pos - camera_pos);
}
void transform_camera2screen(struct ref_point *points, int size)
{
for(int i = 0; i < size; i++) {
vec3 r = points[i].pos;
r.x = num(azrp_width / 2) + r.x * SCREEN_SCALING / r.y;
r.z = num(azrp_height / 2) - r.z * SCREEN_SCALING / r.y;
points[i].pos = r;
}
}

60
src/horizon.cpp Normal file
View File

@ -0,0 +1,60 @@
#include "afterburner.h"
#include <azur/gint/render.h>
#include <num/num.h>
#include <math.h>
using namespace libnum;
uint8_t AB_HORIZON_SHADER_ID = -1;
struct ab_horizon_cmd
{
/* Shader ID for Azur */
uint8_t shader_id;
uint8_t _[3];
/* Variation in value for a single move of +1x */
num dx;
/* Variation in value for a row move of -198x and +1y */
num drow;
/* Palette, supporting indexes 0...255 */
uint16_t *palette;
/* Current horizon value */
num current;
};
extern "C" {
extern azrp_shader_t ab_horizon_shader;
}
GCONSTRUCTOR
static void register_shader(void)
{
AB_HORIZON_SHADER_ID = azrp_register_shader(ab_horizon_shader);
}
void ab_horizon(int cx, int cy, float alpha, uint16_t *palette)
{
prof_enter(azrp_perf_cmdgen);
num sin_alpha = sinf(alpha);
num cos_alpha = cosf(alpha);
num dx = -sin_alpha;
num dy = -cos_alpha;
struct ab_horizon_cmd cmd;
cmd.shader_id = AB_HORIZON_SHADER_ID;
cmd.current = dx * -cx + dy *-cy;
cmd.dx = dx;
cmd.drow = -198*dx + dy;
cmd.palette = palette;
azrp_queue_command(&cmd, sizeof cmd, 0, azrp_frag_count);
prof_leave(azrp_perf_cmdgen);
}
void ab_horizon_configure(void)
{
uint32_t value = (azrp_width << 16) | azrp_frag_height;
azrp_set_uniforms(AB_HORIZON_SHADER_ID, (void *)value);
}

94
src/horizon.s Normal file
View File

@ -0,0 +1,94 @@
/* Horizon shader
r0: (temporary)
r1: cmd.dx
r2: cmd.drow
r3: palette
r4: azrp_width
r5: cmd
r6: azrp_frag
r7: azrp_frag_height
r8: cmd.current
r9: a constant
r10: mask for color index access
*/
.global _ab_horizon_shader
.balign 4
_ab_horizon_shader:
/* r4: azrp_width (top 16 bits) | azrp_frag_height (bottom 16 bits)
r5: struct ab_horizon_cmd *cmd
r6: uint16_t *fragment */
mov.l r8, @-r15
add #4, r5
mov.l r9, @-r15
extu.w r4, r7
mov.l @r5+, r1 /* cmd.dx */
shlr16 r4
mov.l @r5+, r2 /* cmd.drow */
add #-2, r6
mov.l @r5+, r3 /* cmd.palette */
nop
mov.l @r5, r8 /* num.current */
nop
mov.l r10, @-r15 /* r10 = 0x1ff (511 colors) */
mov #-1, r10
ldrs 1f
mov #-23, r0
ldre 2f
shld r0, r10
mov.l .round, r9
nop
.row:
ldrc r4
nop
1: add #2, r6
mov r8, r0
add r9, r0
nop
shlr16 r0
nop
and r10, r0
nop
shll r0
mov.w @(r0, r3), r0
mov.w r0, @r6
2: add r1, r8
dt r7
nop
bf.s .row
add r2, r8
/* Update cmd.current for the next fragment */
mov.l r8, @r5
nop
mov.l @r15+, r10
mov.l @r15+, r9
rts
mov.l @r15+, r8
.balign 4
.round:
/* Add 256 and also round up the next unit */
.long 0x01008000

56
src/image.cpp Normal file
View File

@ -0,0 +1,56 @@
#include "afterburner.h"
#include <gint/display.h>
#include <gint/image.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
//---
// Dynamic images
//---
DynamicImage::DynamicImage(bopti_image_t const *source, float alpha,
prof_t *prof_ctx, int anchor_x, int anchor_y):
m_anchor_x {anchor_x}, m_anchor_y {anchor_y}, m_source {source},
m_alpha {alpha}, m_prof {prof_ctx}
{
for(int i = 0; i < 16; i++) {
m_images[i] = NULL;
m_ax[i] = -1;
m_ay[i] = -1;
}
}
bopti_image_t *DynamicImage::getAtScale(num scale, int *ax, int *ay)
{
if(m_prof)
prof_enter(*m_prof);
/* Round to the closest multiple of 0.125 */
int i = (int)(8 * scale + num(0.5));
if(i < 0) i = 0;
if(i > 15) i = 15;
if(!m_images[i]) {
int ax_i=m_anchor_x, ay_i=m_anchor_y;
struct image_linear_map map;
image_rotate_around_scale(m_source, m_alpha, scale.v, false, &ax_i,
&ay_i, &map);
m_images[i] = image_linear_alloc(m_source, &map);
m_ax[i] = ax_i;
m_ay[i] = ay_i;
}
if(m_prof)
prof_leave(*m_prof);
*ax = m_ax[i];
*ay = m_ay[i];
return m_images[i];
}
DynamicImage::~DynamicImage()
{
for(int i = 0; i < 16; i++)
image_free(m_images[i]);
}

722
src/main.cpp Normal file
View File

@ -0,0 +1,722 @@
#include "afterburner.h"
#include <azur/azur.h>
#include <azur/gint/render.h>
#include <gint/drivers/r61524.h>
#include <gint/keyboard.h>
#include <gint/usb.h>
#include <gint/usb-ff-bulk.h>
#include <gint/cpu.h>
#include <gint/timer.h>
#include <gint/kmalloc.h>
#include <gint/rtc.h>
#include <libprof.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <fxlibc/printf.h>
static char logbuf[512];
GUNUSED static void logger(char const *fmt, ...)
{
if(!usb_is_open())
return;
va_list args;
va_start(args, fmt);
vsnprintf(logbuf, sizeof logbuf, fmt, args);
va_end(args);
usb_fxlink_text(logbuf, 0);
}
#define RGB24(hex) \
(((hex & 0xf80000) >> 8) | \
((hex & 0x00fc00) >> 5) | \
((hex & 0x0000f8) >> 3))
/* Generate the gradient used for the horizon. This is an array of 512 colors,
256 of which represent the ground, while the other 256 represent the sky. */
static void generate_horizon_gradient(uint16_t *horizon_colors)
{
int hc = 0;
for(int i = 0; i < 256; i++) {
horizon_colors[hc++] = RGB24(0x438747);
}
for(int i = 0; i < 32; i++) {
int r = 0x68 + (0x3f - 0x68) * i / 32;
int g = 0x8c + (0x66 - 0x8c) * i / 32;
int b = 0xd6 + (0xb3 - 0xd6) * i / 32;
horizon_colors[hc++] = RGB24(((r << 16) | (g << 8) | b));
}
for(int i = 0; i < 64; i++) {
horizon_colors[hc++] = RGB24(0x3f66b3);
};
for(int i = 0; i < 64; i++) {
int r = 0x3f + (0x0c - 0x3f) * i / 64;
int g = 0x66 + (0x29 - 0x66) * i / 64;
int b = 0xb3 + (0x61 - 0xb3) * i / 64;
horizon_colors[hc++] = RGB24(((r << 16) | (g << 8) | b));
}
for(int i = 0; i < 96; i++) {
horizon_colors[hc++] = RGB24(0x0c2961);
}
}
/* A plane controller that individually controls each direction.
* Up/Down: move forward/backward (including pitch)
* Left/Right: move left/right (ignoring roll; parallel to the ground)
* SHIFT+Up/Down: pitch down/up
* SHIFT+Left/Right: turn left/right (yaw)
* ALPHA+Up/Down: move forward/backward (ignoring pitch)
* ALPHA+Left/Right: roll */
GUNUSED static void controller_debug(struct plane *plane, num dt)
{
int sh = keydown(KEY_SHIFT);
int al = keydown(KEY_ALPHA);
float dt_f = (float)dt;
/* Movement speed for movement controlled here */
num debug_speed = 6.0;
if(keydown(KEY_UP) && !sh && !al)
plane->pos += plane->forward * debug_speed * dt;
if(keydown(KEY_DOWN) && !sh && !al)
plane->pos -= plane->forward * debug_speed * dt;
if(keydown(KEY_RIGHT) && !sh && !al)
plane->pos += plane->right * debug_speed * dt;
if(keydown(KEY_LEFT) && !sh && !al)
plane->pos -= plane->right * debug_speed * dt;
if(keydown(KEY_UP) && sh && !al)
plane->pitch -= 0.3 * dt_f;
if(keydown(KEY_DOWN) && sh && !al)
plane->pitch += 0.3 * dt_f;
if(keydown(KEY_RIGHT) && sh && !al)
plane->yaw -= 0.5 * dt_f;
if(keydown(KEY_LEFT) && sh && !al)
plane->yaw += 0.5 * dt_f;
if(keydown(KEY_UP) && !sh && al)
plane->pos += plane->forward_ground * debug_speed * dt;
if(keydown(KEY_DOWN) && !sh && al)
plane->pos -= plane->forward_ground * debug_speed * dt;
if(keydown(KEY_LEFT) && !sh && al)
plane->roll += 0.45 * dt_f;
if(keydown(KEY_RIGHT) && !sh && al)
plane->roll -= 0.45 * dt_f;
if(keydown(KEY_MINUS))
plane->pos.z -= 2.0 * dt;
if(keydown(KEY_PLUS))
plane->pos.z += 2.0 * dt;
}
/* A simple plane controller for players.
* Up/Down: pitch down/up
* Left/Right: turn left/right (yaw)
* SHIFT/ALPHA: accelerate/decelerate
TODO: Automatic roll effect when turning */
static void controller_simple(struct plane *plane, num dt)
{
float dt_f = (float)dt;
if(keydown(KEY_UP))
plane->pitch -= 0.3 * dt_f;
if(keydown(KEY_DOWN))
plane->pitch += 0.3 * dt_f;
if(keydown(KEY_RIGHT))
plane->yaw -= 0.5 * dt_f;
if(keydown(KEY_LEFT))
plane->yaw += 0.5 * dt_f;
if(keydown(KEY_SHIFT))
plane->air_speed += num(4.0) * dt;
if(keydown(KEY_ALPHA))
plane->air_speed -= num(4.0) * dt;
/* Lock roll to player turning, for more immersion and visual effects
while keeping very simple controls */
float target_roll = 0;
if(keydown(KEY_LEFT))
target_roll = 0.5;
if(keydown(KEY_RIGHT))
target_roll = -0.5;
if(plane->roll < target_roll) {
plane->roll += 0.3 * dt_f;
if(plane->roll > target_roll)
plane->roll = target_roll;
}
if(plane->roll > target_roll) {
plane->roll -= 0.3 * dt_f;
if(plane->roll < target_roll)
plane->roll = target_roll;
}
}
/* Sort to have closer objects last */
static int compare_depth(void const *p1, void const *p2)
{
struct ref_point const *u = (struct ref_point const *)p1;
struct ref_point const *v = (struct ref_point const *)p2;
/* Return v < u */
return v->pos.y.v - u->pos.y.v;
}
#define remap(point, _x, _y) { \
*(_x) = wx + (int)((point).x) * ww / world->w; \
*(_y) = wy + wh-1 - (int)((point).y) * wh / world->h; \
}
/* Generate the minimap as a bopti image (Azur can't easily draw like in the
VRAM, and I don't have time to write a full shader for that) */
static bopti_image_t *generate_minimap(struct world const *world,
struct plane const *plane, struct objective *objectives,
int objective_count)
{
extern bopti_image_t img_minimap;
/* Duplicate the empty minimap (which is full RGB565) */
image_t *img = image_copy_alloc(&img_minimap, IMAGE_RGB565A);
/* Location of world map within image */
int wx=9, wy=9, ww=30, wh=30, x, y;
for(int i = 0; i < objective_count; i++) {
remap(objectives[i].pos, &x, &y);
int color = objectives[i].destroyed ? C_RGB(15,3,31) : C_RGB(31,2,5);
image_set_pixel(img, x, y, color);
}
for(int i = 0; i < 32; i++) {
remap(plane->pos + num(i) * plane->forward, &x, &y);
image_set_pixel(img, x, y, C_RGB(0, 18, 4));
}
remap(plane->pos, &x, &y);
image_set_pixel(img, x, y, C_RGB(0, 31, 10));
return img;
}
/* Draw player's plane at (x,y) */
static void draw_player(int x, int y, struct plane *plane,
bool orient_with_angles, bool orient_with_keyboard)
{
/* Rows: 25 (anchor 17), 27 (anchor 20), 26 (anchor 21)
Columns: all 48 (anchor 23) */
extern bopti_image_t img_plane;
int row = 1, col = 1;
if(orient_with_angles) {
if(plane->pitch > 0.08)
row = 0;
if(plane->pitch < -0.08)
row = 2;
if(plane->roll > 0.1)
col = 0;
if(plane->roll < -0.1)
col = 2;
}
if(orient_with_keyboard) {
if(keydown(KEY_DOWN))
row = 0;
if(keydown(KEY_UP))
row = 2;
if(keydown(KEY_LEFT))
col = 0;
if(keydown(KEY_RIGHT))
col = 2;
}
int top=0, height=0, ay=0;
if(row == 0) top = 0, height = 25, ay = 17;
if(row == 1) top = 25, height = 27, ay = 20;
if(row == 2) top = 52, height = 26, ay = 21;
azrp_subimage(x-23, y-ay, &img_plane, 48 * col, top, 48, height,
DIMAGE_NONE);
}
/* Distance between two points, with wrap-around */
num64 distance2_wrap_64(struct world const *world, vec3 u, vec3 v)
{
num dx = u.x - v.x;
num dy = u.y - v.y;
num dz = u.z - v.z;
/* Choose the closest x distance possible */
if(dx < num(-world->w))
dx += num(world->w);
if(dx > num(world->w))
dx -= num(world->w);
/* Choose the closest y distance possible */
if(dy < num(-world->h))
dy += num(world->h);
if(dy > num(world->h))
dy -= num(world->h);
return num32::dmul(dx, dx) + num32::dmul(dy, dy) + num32::dmul(dz, dz);
}
/* Render text with Azur images - quite bad, but I don't have time lol. */
static void draw_text(int x, int y, char const *fmt, ...)
{
char str[128];
va_list args;
va_start(args, fmt);
vsnprintf(str, 128, fmt, args);
va_end(args);
extern bopti_image_t img_font;
for(int i = 0; str[i]; i++) {
if(str[i] < 32 || str[i] >= 0x7f) continue;
int row = (str[i] - 32) >> 4;
int col = (str[i] - 32) & 15;
azrp_subimage(x + 5 * i, y, &img_font, 7 * col + 1, 9 * row + 1, 6, 8,
DIMAGE_NONE);
}
}
int main(void)
{
azrp_config_scale(2);
azrp_shader_clear_configure();
azrp_shader_image_rgb16_configure();
azrp_shader_image_p8_configure();
azrp_shader_image_p4_configure();
ab_horizon_configure();
__printf_enable_fp();
srand(rtc_ticks());
prof_init();
usb_interface_t const *intf[] = { &usb_ff_bulk, NULL };
usb_open(intf, GINT_CALL_NULL);
int volatile timer_flag = 1;
int timer = timer_configure(TIMER_ANY, 33000, GINT_CALL_SET(&timer_flag));
timer_start(timer);
bool lost = false;
//---
// Horizon colors
//---
uint16_t horizon_colors[512];
generate_horizon_gradient(horizon_colors);
//---
// World map
//---
/* Stuff starts to blow up at the density of 1/16 sprite/unit² (4096
sprites for a 256x256 world): even with chunk-based selection, we end up
with > 2000 points to consider each frame, which makes the 3D transform
blow up to 16-20 ms per frame. That's the obvious next optimization. */
int point_count = 2048;
struct ref_point *points = (struct ref_point *)
malloc(point_count * sizeof *points);
/* We give 12 bases to destroy */
struct objective objectives[12];
int objective_count = 12;
/* We allow 3 explosions at the same time */
struct explosion explosions[3];
int explosion_count = 3;
for(int i = 0; i < explosion_count; i++) {
explosions[i].pos = vec3();
explosions[i].time = num(-1);
}
for(int i = 0; i < point_count; i++) {
num x, y;
x.v = rand() % num(256).v;
y.v = rand() % num(256).v;
points[i].pos = vec3(x, y, 0);
if(i < objective_count) {
objectives[i].pos = points[i].pos;
objectives[i].destroyed = false;
objectives[i].type = rand() % 3;
points[i].id = 100 + i;
}
else {
points[i].id = rand() % 6;
}
}
struct world *world = world_make(256, 256, points, point_count);
if(!world) return 0;
//---
// Cached explosion images
//---
prof_t perf_scaling;
/* Unlike normal sprites, we don't rotate exlosions, so we can keep them
cached for the whole execution */
extern bopti_image_t img_expl_1, img_expl_2, img_expl_3, img_expl_4;
DynamicImage dyn_expl_1(&img_expl_1, 0.0, &perf_scaling, 24, 24);
DynamicImage dyn_expl_2(&img_expl_2, 0.0, &perf_scaling, 24, 24);
DynamicImage dyn_expl_3(&img_expl_3, 0.0, &perf_scaling, 24, 24);
DynamicImage dyn_expl_4(&img_expl_4, 0.0, &perf_scaling, 31, 31);
DynamicImage *dyn_expl[4] = {
&dyn_expl_1, &dyn_expl_2, &dyn_expl_3, &dyn_expl_4,
};
//---
// HUD and messages
//---
char const *message = NULL;
num message_time = num(0.0);
//---
// Camera
//---
struct plane plane;
plane.pos = vec3(world->w / 2, world->h / 2, 5);
plane.roll = 0.0f;
plane.pitch = 0.0f;
plane.yaw = 0.0f;
plane.air_speed = 12.0;
int plane_screen_x = 198/2;
int plane_screen_y = 112/2;
while(1) {
/* Wait until a new frame to cap at the desired FPS */
while(!timer_flag) sleep();
//--- Total frame time
prof_t perf_frame = prof_make();
prof_enter(perf_frame);
/* Time difference for this frame (assumes consistant FPS) */
num dt = 33e-3;
//--- Physics/modeling time (split in two parts)
prof_t perf_physics = prof_make();
prof_enter(perf_physics);
plane_update(&plane);
vec3 camera_pos = plane.pos - num(10) * plane.forward;
/* Compute horizon reference */
struct ref_point far_forward;
far_forward.pos = plane.pos + plane.forward * num(FAR_CLIP_PLANE);
far_forward.pos.z = 0;
transform_world2camera(&plane, camera_pos, &far_forward, 1);
transform_camera2screen(&far_forward, 1);
int visible_explosions = 0;
for(int i = 0; i < explosion_count; i++)
visible_explosions += (explosions[i].time >= num(0));
/* Gather world points in chunks close to the plane */
int sprite_count;
struct ref_point *sprites = world_select(world, plane.pos,
plane.forward, &sprite_count, 1 + visible_explosions);
/* Extra sprite for the target under the plane */
sprites[sprite_count].pos =
plane.pos + num(BOMB_DISTANCE) * plane.forward;
sprites[sprite_count].pos.z = 0;
sprites[sprite_count].id = -1;
sprite_count++;
/* Extra sprites for visible explosions */
for(int i = 0; i < explosion_count; i++) {
if(explosions[i].time < num(0))
continue;
int n = (int)(explosions[i].time * 6) % 4;
sprites[sprite_count].pos = explosions[i].pos;
sprites[sprite_count].id = 9 + n;
sprite_count++;
}
transform_world2camera(&plane, camera_pos, sprites, sprite_count);
/* Prune sprites that are definitely not visible */
int close_point_count = 0;
for(int i = 0; i < sprite_count; i++) {
if(sprites[i].pos.y < num(NEAR_CLIP_PLANE) ||
sprites[i].pos.y >= num(FAR_CLIP_PLANE) ||
abs((int)sprites[i].pos.x) >= SIDE_CLIP_PLANE)
continue;
sprites[close_point_count++] = sprites[i];
}
transform_camera2screen(sprites, close_point_count);
/* Prune points that are also too far awar from the screen */
int visible_point_count = 0;
for(int i = 0; i < close_point_count; i++) {
int x = (int)sprites[i].pos.x;
int z = (int)sprites[i].pos.z;
if(x < -SCREEN_MARGIN || x >= azrp_width + SCREEN_MARGIN
|| z < -SCREEN_MARGIN || z >= azrp_height + SCREEN_MARGIN)
continue;
sprites[visible_point_count++] = sprites[i];
}
/* Sort sprites furthest to closest */
qsort(sprites, visible_point_count, sizeof *sprites, compare_depth);
prof_leave(perf_physics);
extern bopti_image_t img_dot;
extern bopti_image_t img_tree_1, img_tree_2, img_tree_3;
extern bopti_image_t img_mountain_1;
extern bopti_image_t img_rock_1, img_rock_2;
extern bopti_image_t img_base_1, img_base_2, img_base_3;
/* Resize images for all visible sprites */
perf_scaling = prof_make();
DynamicImage dyn_tree_1(&img_tree_1,
-plane.roll, &perf_scaling, 16, 31);
DynamicImage dyn_tree_2(&img_tree_2,
-plane.roll, &perf_scaling, 16, 31);
DynamicImage dyn_tree_3(&img_tree_3,
-plane.roll, &perf_scaling, 16, 31);
DynamicImage dyn_mountain_1(&img_mountain_1,
-plane.roll, &perf_scaling, 24, 34);
DynamicImage dyn_rock_1(&img_rock_1,
-plane.roll, &perf_scaling, 15, 20);
DynamicImage dyn_rock_2(&img_rock_2,
-plane.roll, &perf_scaling, 11, 15);
DynamicImage dyn_base_1(&img_base_1,
-plane.roll, &perf_scaling, 16, 20);
DynamicImage dyn_base_2(&img_base_2,
-plane.roll, &perf_scaling, 16, 20);
DynamicImage dyn_base_3(&img_base_3,
-plane.roll, &perf_scaling, 16, 20);
DynamicImage *dyn_images[9] = {
&dyn_tree_1, &dyn_tree_2, &dyn_tree_3,
&dyn_mountain_1,
&dyn_rock_1, &dyn_rock_2,
&dyn_base_1, &dyn_base_2, &dyn_base_3,
};
/* Generate the minimap */
image_t *minimap = generate_minimap(world, &plane, objectives,
objective_count);
//---
azrp_perf_clear();
ab_horizon((int)far_forward.pos.x, (int)far_forward.pos.z, -plane.roll,
horizon_colors);
for(int i = 0; i < visible_point_count; i++) {
int ax, ay;
num scale = 15.0;
/* Make targeted buildings larger */
if(sprites[i].id >= 6 && sprites[i].id < 9)
scale = 25.0;
scale = scale / sprites[i].pos.y;
bopti_image_t *img = NULL;
if(sprites[i].id == -1) {
img = &img_dot;
ax = 2;
ay = 2;
}
else if(sprites[i].id < 6) {
img = dyn_images[sprites[i].id]->getAtScale(scale,&ax,&ay);
}
else if(sprites[i].id >= 9 && sprites[i].id < 13) {
img = dyn_expl[sprites[i].id - 9]->getAtScale(scale, &ax, &ay);
}
else if(sprites[i].id >= 100) {
struct objective *o = &objectives[sprites[i].id - 100];
if(!o->destroyed)
img = dyn_images[6+o->type]->getAtScale(scale,&ax,&ay);
}
if(img) azrp_image((int)sprites[i].pos.x - ax,
(int)sprites[i].pos.z - ay, img);
}
if(!lost)
draw_player(plane_screen_x, plane_screen_y, &plane, false, !lost);
if(message)
draw_text(2, 2, "%s", message);
else
draw_text(2, 2, "Speed:%d", (int)(plane.air_speed * 40));
azrp_image(azrp_width - minimap->width - 2, 2, minimap);
/* Shows a dot the forward direction on the ground */
// azrp_image((int)far_forward.x-2, (int)far_forward.z-2, &img_dot);
azrp_update();
free(sprites);
image_free(minimap);
//---
// Event handling and physics
//---
bool explosion_trigger = false;
key_event_t ev;
while((ev = pollevent()).type != KEYEV_NONE) {
if(ev.type == KEYEV_DOWN && ev.key == KEY_F1)
explosion_trigger = true;
}
clearevents();
if(keydown(KEY_MENU) || keydown(KEY_EXIT))
break;
prof_enter(perf_physics);
if(!lost)
controller_simple(&plane, dt);
/* Limit pitch to reasonable ranges (extreme values cause graphical
glitches with the horizon, and accentuate the lens problem */
if(plane.pitch < -0.2)
plane.pitch = -0.2;
if(plane.pitch > 0.2)
plane.pitch = 0.2;
/* Limit air speed to "reasonable" defaults */
if(plane.air_speed < num(6.0))
plane.air_speed = 6.0;
if(plane.air_speed > num(24.0))
plane.air_speed = 24.0;
if(!lost)
plane.pos += plane.air_speed * dt * plane.forward;
/* Wrap the plane around the world map */
if((int)plane.pos.x < 0)
plane.pos.x += world->w;
if((int)plane.pos.x >= world->w)
plane.pos.x -= world->w;
if((int)plane.pos.y < 0)
plane.pos.y += world->h;
if((int)plane.pos.y >= world->h)
plane.pos.y -= world->h;
/* Add new explosions */
if(!lost && explosion_trigger) {
int id = -1;
for(int i = 0; i < explosion_count; i++) {
if(explosions[i].time < num(0)) {
id = i;
break;
}
}
if(id >= 0) {
explosions[id].pos =
plane.pos + num(BOMB_DISTANCE) * plane.forward;
explosions[id].pos.z = 0;
explosions[id].time = 0.0;
bool hit_someone = false;
/* Destroy bases that are close! */
for(int i = 0; i < objective_count; i++) {
if(distance2_64(objectives[i].pos,
explosions[id].pos) < num64(20)) {
objectives[i].destroyed = true;
hit_someone = true;
}
}
message = hit_someone ? "Hit!" : "Missed!";
message_time = num(2.0);
}
}
/* Maintain the time of current explosions */
for(int i = 0; i < explosion_count; i++) {
if(explosions[i].time >= num(0)) {
explosions[i].time += dt;
if(explosions[i].time >= num(4.0/6.0))
explosions[i].time = -1;
}
}
/* Maintain the message time */
if(message != NULL) {
message_time -= dt;
if(message_time < num(0)) {
message = NULL;
message_time = num(0.0);
}
}
prof_leave(perf_physics);
prof_leave(perf_frame);
#if 0
logger("Frame total: %d µs\n - Rendering: update %d µs, shaders %d µs"
"\n - Physics: %d µs\n - Sprite scaling: %d µs\n"
"Current info:\n Position: x=%g y=%g z=%g\n Angles: roll=%g "
"pitch=%g yaw=%g\n Air speed: %g\n"
"Points: %d selected, %d close, %d visible\n"
"Explosions: %d\n",
prof_time(perf_frame),
prof_time(azrp_perf_render),
prof_time(azrp_perf_shaders),
prof_time(perf_physics),
prof_time(perf_scaling),
(double)plane.pos.x,
(double)plane.pos.y,
(double)plane.pos.z,
(double)plane.roll,
(double)plane.pitch,
(double)plane.yaw,
(double)plane.air_speed,
sprite_count,
close_point_count,
visible_point_count,
visible_explosions);
#endif
/* Victory condition */
bool all_objectives_destroyed = true;
for(int i = 0; i < objective_count; i++)
all_objectives_destroyed &= objectives[i].destroyed;
if(all_objectives_destroyed) {
message = "Mission accomplished!";
message_time = num(32000);
}
/* Death condition */
if(!lost && plane.pos.z <= num(0.5)) {
message = "Unfortunate.";
message_time = num(32000);
lost = true;
/* Hijack one of the explosions */
explosions[0].pos = plane.pos;
explosions[0].time = num(0.0);
}
}
timer_stop(timer);
return 1;
}

163
src/world.cpp Normal file
View File

@ -0,0 +1,163 @@
#include "afterburner.h"
#include <stdlib.h>
int debug = 0;
void *debug2 = NULL;
struct world *world_make(int width, int height, struct ref_point *points,
int count)
{
if(!points)
return NULL;
struct world *w = (struct world *)malloc(sizeof *w);
int *js = NULL;
size_t s;
if(!w) goto fail;
w->w = width;
w->h = height;
w->chunks_x = (width + CHUNK_SIZE - 1) / CHUNK_SIZE;
w->chunks_y = (height + CHUNK_SIZE - 1) / CHUNK_SIZE;
s = w->chunks_x * w->chunks_y * sizeof *w->chunks;
w->chunks = (struct chunk *)malloc(s);
if(!w->chunks) goto fail;
for(int i = 0; i < w->chunks_x * w->chunks_y; i++) {
w->chunks[i].points = NULL;
w->chunks[i].count = 0;
}
/* Do a first pass to count the number of points in each chunks */
for(int i = 0; i < count; i++) {
int cx = (int)points[i].pos.x / CHUNK_SIZE;
int cy = (int)points[i].pos.y / CHUNK_SIZE;
w->chunks[cy * w->chunks_x + cx].count++;
}
/* Allocate all chunks' data */
for(int i = 0; i < w->chunks_x * w->chunks_y; i++) {
if(w->chunks[i].count == 0)
continue;
w->chunks[i].points = (struct ref_point *)malloc(
w->chunks[i].count * sizeof(struct ref_point));
if(!w->chunks[i].points) goto fail;
}
/* Do a second pass to actually fill the chunks */
s = w->chunks_x * w->chunks_y * sizeof(int);
js = (int *)malloc(s);
if(!js) goto fail;
for(int i = 0; i < w->chunks_x * w->chunks_y; i++) {
js[i] = 0;
}
for(int i = 0; i < count; i++) {
int cx = (int)points[i].pos.x / CHUNK_SIZE;
int cy = (int)points[i].pos.y / CHUNK_SIZE;
int j = cy * w->chunks_x + cx;
w->chunks[j].points[js[j]++] = points[i];
}
free(js);
return w;
fail:
world_destroy(w);
return NULL;
}
void world_destroy(struct world *w)
{
if(!w) return;
if(w->chunks) {
for(int i = 0; i < w->chunks_x * w->chunks_y; i++)
free(w->chunks[i].points);
free(w->chunks);
}
free(w);
}
struct ref_point *world_select(struct world const *world, vec3 position,
GUNUSED vec3 forward, int *size, int extra_size)
{
int cx1 = (int)position.x / CHUNK_SIZE;
int cy1 = (int)position.y / CHUNK_SIZE;
// int fx = (int)forward.x >= 0 ? +1 : -1;
// int fy = (int)forward.y >= 0 ? +1 : -1;
int chunks[9];
int chunk_count = 0;
for(int dy = -1; dy <= +1; dy++)
for(int dx = -1; dx <= +1; dx++) {
int cy = cy1 + dy;
int cx = cx1 + dx;
if(cx < 0)
cx = world->chunks_x - 1;
if(cx >= world->chunks_x)
cx = 0;
if(cy < 0)
cy = world->chunks_y - 1;
if(cy >= world->chunks_y)
cy = 0;
chunks[chunk_count++] = cy * world->chunks_x + cx;
}
/* Count the points to be allocated */
int total_point_count = 0;
for(int i = 0; i < chunk_count; i++)
total_point_count += world->chunks[chunks[i]].count;
/* Allocate the array */
struct ref_point *output = (struct ref_point *)malloc(
(total_point_count + extra_size) * sizeof *output);
/* Fill the points in selected chunks. While doing this, we also rotate the
points. The player is constrained to be within the world boundary, but
it can still be on the edge and look outside. In this situation, we
rotate the points to give the illusion that the world wraps around. */
num world_size_x14 = num(world->w / 4);
num world_size_x34 = num(3 * world->w / 4);
num world_size_y14 = num(world->h / 4);
num world_size_y34 = num(3 * world->h / 4);
/* This simple test only works if the map is at least 4x as large as
the depth of view of the plane, (here 256 > 4×50). */
num wrap_x_minus = (position.x < world_size_x14)
? world_size_x34 : num(world->w);
num wrap_x_plus = (position.x > world_size_x34)
? world_size_x14 : num(0);
num wrap_y_minus = (position.y < world_size_y14)
? world_size_y34 : num(world->h);
num wrap_y_plus = (position.y > world_size_y34)
? world_size_y14 : num(0);
int k = 0;
for(int i = 0; i < chunk_count; i++) {
struct chunk *c = &world->chunks[chunks[i]];
for(int j = 0; j < c->count; j++) {
struct ref_point p = c->points[j];
if(p.pos.x < wrap_x_plus)
p.pos.x += num(world->w);
else if(p.pos.x > wrap_x_minus)
p.pos.x -= num(world->w);
if(p.pos.y < wrap_y_plus)
p.pos.y += num(world->h);
else if(p.pos.y > wrap_y_minus)
p.pos.y -= num(world->h);
output[k++] = p;
}
}
*size = total_point_count;
return output;
}