AfterBurner/src/main.cpp

723 lines
22 KiB
C++

#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;
}