Skip to content

Instantly share code, notes, and snippets.

@wbbradley
Created April 30, 2020 23:19
Show Gist options
  • Save wbbradley/c2c31ddffbc06c16687b5aab49d75a09 to your computer and use it in GitHub Desktop.
Save wbbradley/c2c31ddffbc06c16687b5aab49d75a09 to your computer and use it in GitHub Desktop.
/**********************************************************************************************
*
* raylib 32x32 game/demo competition
*
* Competition consist in developing a videogame in a 32x32 pixels screen size.
*
* LICENSE: zlib/libpng
*
* Copyright (c) 2020 Will Bradley (@wbbradley)
*
* This software is provided "as-is", without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from the
* use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it freely,
* subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software in a
* product, an acknowledgment in the product documentation would be appreciated
* but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not
* be misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source
* distribution.
*
**********************************************************************************************/
#include "raylib.h"
#include "raymath.h"
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define PLAYER_MAX 4
#define MENU_DEMO_MONSTERS 4
#define GAME_SIZE 32
#define ANON_BULLETS 0
#define CLOSE_ENOUGH 1.0
#define BOUNCY_WALLS 0
#define BULLET_FREQ 6
#define BULLET_MAX_AGE 60*14
#define STICKY_BULLETS 1
#define STICKY_WALLS (STICKY_BULLETS && 0)
#define MAX_MOTION_SPEED 0.25
#define BULLET_SPEED (MAX_MOTION_SPEED * 1.5)
#define INCLUDE_MOVEMENT_IN_SHOOTING 0
#define max(a, b) ((a) > (b) ? (a) : (b))
#define min(a, b) ((a) < (b) ? (a) : (b))
const int windowWidth = 768;
const int windowHeight = 768;
const int gameScreenWidth = GAME_SIZE;
const int gameScreenHeight = GAME_SIZE;
#ifdef __APPLE__
// HACKHACK: get the correct HiDPI scaling ratio for this machine...
const float extra_scale = 2.0;
#else
const float extra_scale = 1.0;
#endif
static Color *player_colors = NULL;
static Color *bullet_colors = NULL;
static Color wall_color = {0xce, 0xce, 0xce, 0xff};
static const float gamepad_fudge = 0.3;
struct State {
bool menu_visible;
bool game_paused;
int player_max_selection;
struct Game *game;
};
struct Vec2D {
float x, y;
};
static const struct Vec2D origin = {0.0, 0.0};
struct Player {
struct Vec2D pos;
bool is_monster;
int gamepad;
int last_bullet_frame;
struct Color color;
};
struct Game {
int frame_count;
int humans;
int monsters;
int score;
bool game_over;
struct Player player[PLAYER_MAX];
struct Bullet *bullets;
};
int GameGetPlayerCount(struct Game *game) {
return game->humans + game->monsters;
}
struct Bullet {
struct Vec2D pos;
struct Vec2D dir;
int player;
int alive_until_frame;
bool dead;
struct Bullet *next;
};
const int ALIVE_UNTIL_FOREVER = -1;
Color palette_lerp(const Color *palette, int palette_count, float alpha) {
int index = (float)palette_count * Clamp(alpha, 0.0, 1.0);
if (index >= palette_count) {
index = palette_count - 1;
}
return palette[index];
}
float sqr(float x) {
return x * x;
}
float vec2d_length_squared(struct Vec2D a) {
return sqr(a.x)+sqr(a.y);
}
float vec2d_length(struct Vec2D a) {
return sqrt(vec2d_length_squared(a));
}
float distance_squared(struct Vec2D *a, struct Vec2D *b) {
return sqr(a->x-b->x)+sqr(a->y-b->y);
}
float distance(struct Vec2D *a, struct Vec2D *b) {
return sqrt(distance_squared(a, b));
}
struct Vec2D scale(float a, struct Vec2D b) {
return (struct Vec2D){.x=b.x*a, .y=b.y*a};
}
struct Vec2D roundto(struct Vec2D a, float z) {
return (struct Vec2D){
.x = round(a.x / z) * z,
.y = round(a.y / z) * z,
};
}
struct Vec2D subtract(struct Vec2D a, struct Vec2D b) {
return (struct Vec2D){.x=a.x-b.x, .y=a.y-b.y};
}
struct Vec2D add(struct Vec2D a, struct Vec2D b) {
return (struct Vec2D){.x=a.x+b.x, .y=a.y+b.y};
}
struct Vec2D rotate90(struct Vec2D a) {
return (struct Vec2D){
.x = -a.y,
.y = a.x,
};
}
struct Vec2D rotateneg90(struct Vec2D a) {
return (struct Vec2D){
.x = a.y,
.y = -a.x,
};
}
float dot(struct Vec2D a, struct Vec2D b) {
return a.x*b.x+a.y*b.y;
}
struct Vec2D normalize(struct Vec2D a) {
float len = sqrt(sqr(a.x)+sqr(a.y));
return (struct Vec2D){.x=a.x/len,.y=a.y/len};
}
struct Vec2D clamp_length(struct Vec2D a, float min_length, float max_length) {
float len_squared = sqr(a.x) + sqr(a.y);
if (len_squared >= sqr(min_length)) {
if (len_squared <= sqr(max_length)) {
/* within range */
return a;
} else {
return scale(max_length, normalize(a));
}
} else {
return scale(min_length, normalize(a));
}
}
struct Vec2D ClampMotion(struct Vec2D movement) {
return scale(MAX_MOTION_SPEED, clamp_length(movement, 0.0, 1.0));
}
bool IsCloseEnough(struct Vec2D *a, struct Vec2D *b) {
return distance(a, b) < CLOSE_ENOUGH;
}
bool IsOffscreen(struct Vec2D a) {
return a.x < 0 || a.y < 0 || a.x > gameScreenWidth || a.y > gameScreenHeight;
}
bool IsBulletSolid(struct Bullet *bullet, int player) {
if (bullet->dead) {
return false;
}
#if ANON_BULLETS
return true;
#else
/* player's bullets are not "solid" to them in non-anonomyous bullet mode */
return bullet->player != player;
#endif
}
bool IsBulletWall(struct Bullet *bullet) {
return !bullet->dead && vec2d_length_squared(bullet->dir) <= 1E-5;
}
bool IsBulletArmed(struct Bullet *bullet, int player) {
return !bullet->dead && !IsBulletWall(bullet) && IsBulletSolid(bullet, player);
}
bool IsFreePos(struct Game *game, struct Vec2D pos, int ignore_player) {
for (int i = 0; i < game->humans + game->monsters; i++) {
if (i == ignore_player) {
continue;
}
if (IsCloseEnough(&game->player[i].pos, &pos)) {
return false;
}
}
for (struct Bullet *bullet = game->bullets; bullet != NULL;
bullet = bullet->next) {
if (!IsBulletSolid(bullet, ignore_player)) {
continue;
}
if (IsCloseEnough(&bullet->pos, &pos)) {
return false;
}
}
return true;
}
struct Vec2D FindFreeRandomPosForPlayer(struct Game *game, int player) {
do {
struct Vec2D pos = (struct Vec2D){
.x = rand() % gameScreenWidth,
.y = rand() % gameScreenHeight,
};
if (IsFreePos(game, pos, player/*ignore_player*/)) {
return pos;
}
} while (true);
}
struct Vec2D FindFreeRandomPosAwayFromPlayer(struct Game *game) {
struct Vec2D pos;
float away_fraction = 0.7;
l_tryagain:
away_fraction *= 0.9;
pos = FindFreeRandomPosForPlayer(game, -1);
for (int i = 0; i < game->humans + game->monsters; i++) {
if (distance(&pos, &game->player[i].pos) <
min(gameScreenWidth, gameScreenHeight) * away_fraction) {
/* too close! */
goto l_tryagain;
}
}
return pos;
}
void KillBullet(struct Bullet *bullet, bool caused_by_collision) {
if (bullet->alive_until_frame != ALIVE_UNTIL_FOREVER) {
#if STICKY_BULLETS
if (caused_by_collision) {
bullet->pos = roundto(bullet->pos, 0.5);
bullet->dir = origin;
bullet->player = PLAYER_MAX;
} else {
bullet->dead = true;
}
#else
bullet->dead = true;
#endif
} else {
/* alive forever bullets cannot be killed */
}
}
void ResetBullets(struct Bullet *bullet) {
/* kill any bullets that may have been alive in the past game */
for (; bullet != NULL; bullet = bullet->next) {
KillBullet(bullet, false);
}
}
void InitGame(struct Game *game, int humans, int monsters) {
*game = (struct Game){
.game_over = false,
.frame_count = 0,
.score = 0,
.bullets = game->bullets,
};
ResetBullets(game->bullets);
game->humans = 0;
game->monsters = 0;
for (int i = 0; i < humans; i++) {
game->humans = i;
game->player[i] = (struct Player){
.pos = FindFreeRandomPosAwayFromPlayer(game),
.is_monster = false,
.gamepad = GAMEPAD_PLAYER1 + i,
.last_bullet_frame = 0,
.color = player_colors[i],
};
}
game->humans = humans;
for (int i = 0; i < monsters; i++) {
game->monsters = i;
game->player[humans + i] = (struct Player){
.pos = FindFreeRandomPosAwayFromPlayer(game),
.is_monster = true,
.gamepad = 0,
.last_bullet_frame = 0,
.color = player_colors[humans + i],
};
}
game->monsters = monsters;
}
struct Game *NewGame(int humans, int monsters) {
struct Game *game = malloc(sizeof(struct Game));
*game = (struct Game){0};
InitGame(game, humans, monsters);
return game;
}
void InitBullet(struct Bullet *bullet, int frame_count, int player, float x,
float y, float dx, float dy) {
assert(bullet->dead);
struct Vec2D bullet_direction = normalize((struct Vec2D){.x = dx, .y = dy});
struct Vec2D dir = scale(BULLET_SPEED, bullet_direction);
struct Vec2D pos = (struct Vec2D){.x = x, .y = y};
struct Vec2D initial_pos =
add(pos, scale(CLOSE_ENOUGH * 1.5, bullet_direction));
*bullet = (struct Bullet) {
.dead = false, .alive_until_frame = frame_count + BULLET_MAX_AGE,
.pos = initial_pos, .dir = dir,
#if ANON_BULLETS
.player = PLAYER_MAX,
#else
.player = player,
#endif
.next = bullet->next,
};
}
struct Bullet *NewBullet(int frame_count, int player, float x, float y, float dx, float dy) {
static int count = 0;
++count;
struct Bullet *bullet = malloc(sizeof(struct Bullet));
bullet->next = NULL;
bullet->dead = true;
InitBullet(bullet, frame_count, player, x, y, dx, dy);
return bullet;
}
void AddBullet(struct Game *game, int player, float x, float y, float dx,
float dy) {
if (game->player[player].last_bullet_frame + BULLET_FREQ >
game->frame_count) {
return;
}
game->player[player].last_bullet_frame = game->frame_count;
if (game->bullets == NULL) {
game->bullets = NewBullet(game->frame_count, player, x, y, dx, dy);
} else {
struct Bullet *bullet = game->bullets;
for (; bullet != NULL; bullet = bullet->next) {
if (bullet->dead) {
InitBullet(bullet, game->frame_count, player, x, y, dx, dy);
return;
}
}
bullet = NewBullet(game->frame_count, player, x, y, dx, dy);
bullet->next = game->bullets;
game->bullets = bullet;
}
}
void CollideBullets(struct Bullet *bullet, struct Bullet *bullet2) {
if (!IsBulletSolid(bullet, bullet2->player) ||
!IsBulletSolid(bullet2, bullet->player)) {
return;
}
assert(!bullet->dead && !bullet2->dead);
if (!IsCloseEnough(&bullet->pos, &bullet2->pos)) {
return;
}
bool is_moving[2] = {
!IsBulletWall(bullet),
!IsBulletWall(bullet2),
};
/* these bullets are too close */
if (is_moving[0]) {
KillBullet(bullet, STICKY_WALLS || is_moving[1]);
}
if (is_moving[1]) {
KillBullet(bullet2, STICKY_WALLS || is_moving[0]);
}
}
void MoveBullets(struct State *state) {
struct Game *game = state->game;
int count =0;
for (struct Bullet *bullet = game->bullets; bullet != NULL;
bullet = bullet->next) {
if (bullet->alive_until_frame != ALIVE_UNTIL_FOREVER &&
game->frame_count > bullet->alive_until_frame) {
KillBullet(bullet, false /*caused_by_collision*/);
continue;
}
if (bullet->dead) {
continue;
}
/* look for bullet collisions */
for (struct Bullet *bullet2 = bullet->next; bullet2 != NULL;
bullet2 = bullet2->next) {
CollideBullets(bullet, bullet2);
}
if (bullet->dead) {
continue;
}
++count;
bullet->pos.x += bullet->dir.x;
bullet->pos.y += bullet->dir.y;
#if BOUNCY_WALLS
if (bullet->pos.y < 0) {
bullet->pos.y = 0;
bullet->dir.y *= -1;
} else if (bullet->pos.y > gameScreenHeight) {
bullet->pos.y = gameScreenHeight;
bullet->dir.y *= -1;
}
if (bullet->pos.x < 0) {
bullet->pos.x = 0;
bullet->dir.x *= -1;
} else if (bullet->pos.x > gameScreenWidth) {
bullet->pos.x = gameScreenWidth;
bullet->dir.x *= -1;
}
#endif
if (IsOffscreen(bullet->pos)) {
KillBullet(bullet, false);
} else {
for (int i = 0; i < game->humans + game->monsters; i++) {
/* player collision with bullet */
if (IsBulletArmed(bullet, i) &&
IsCloseEnough(&bullet->pos, &game->player[i].pos)) {
game->game_over = true;
InitGame(game, 0, MENU_DEMO_MONSTERS);
state->menu_visible = true;
}
}
}
}
}
struct Motion {
struct Vec2D movement;
struct Vec2D shooting;
};
struct Motion PlanMonsterMoveCore(struct Game *game, int player_index) {
struct Player *monster = &game->player[player_index];
/* find nearest danger */
float nearest_distance_squared = sqr(max(gameScreenWidth, gameScreenWidth));
struct Bullet *nearest_bullet = NULL;
for (struct Bullet *bullet = game->bullets; bullet != NULL;
bullet = bullet->next) {
if (bullet->dead || bullet->player == player_index) {
continue;
}
float d_squared = distance_squared(&bullet->pos, &monster->pos);
if (d_squared < nearest_distance_squared) {
nearest_bullet = bullet;
nearest_distance_squared = d_squared;
}
}
struct Vec2D towards_center =
clamp_length(subtract(
(struct Vec2D){
.x = gameScreenWidth / 2.0,
.y = gameScreenHeight / 2.0,
},
monster->pos),
0.0, 0.2);
int enemy_player_index = rand() % (GameGetPlayerCount(game)-1);
if (enemy_player_index >= player_index) {
enemy_player_index += 1;
}
struct Vec2D dir = towards_center;
if (nearest_bullet != NULL) {
struct Vec2D dodge_bullet;
struct Vec2D towards_bullet =
normalize(subtract(nearest_bullet->pos, monster->pos));
struct Vec2D (*evasion)(struct Vec2D) =
(((game->frame_count / ((player_index + 3) * 60) +
player_index * (rand() % 10 == 1)) &
1)
? rotateneg90
: rotate90);
if (IsBulletWall(nearest_bullet)) {
return (struct Motion){
.movement = scale(0.8, evasion(towards_bullet)),
.shooting = normalize(
subtract(game->player[enemy_player_index].pos, monster->pos)),
};
} else {
dodge_bullet = rotate90(normalize(nearest_bullet->dir));
}
float t = dot(dodge_bullet, towards_bullet);
if (t > 0) {
dodge_bullet = (struct Vec2D){
.x = -dodge_bullet.x,
.y = -dodge_bullet.y,
};
}
dir = normalize(add(dir, dodge_bullet));
return (struct Motion){
.movement = dir,
.shooting = normalize(
subtract(game->player[enemy_player_index].pos, monster->pos)),
};
}
return (struct Motion){
.movement = dir,
.shooting = normalize(
subtract(game->player[enemy_player_index].pos, monster->pos)),
};
}
struct Motion PlanPlayerMove(struct Game *game, int player_index) {
struct Player *player = &game->player[player_index];
if (player->is_monster) {
return PlanMonsterMoveCore(game, player_index);
}
float clx = 0.0, cly = 0.0, crx = 0.0, cry = 0.0;
if (IsKeyDown(KEY_UP)) {
cry = -1.0;
}
if (IsKeyDown(KEY_DOWN)) {
cry = 1.0;
}
if (IsKeyDown(KEY_LEFT)) {
crx = -1.0;
}
if (IsKeyDown(KEY_RIGHT)) {
crx = 1.0;
}
if (IsKeyDown('W')) {
cly = -1.0;
}
if (IsKeyDown('A')) {
clx = -1.0;
}
if (IsKeyDown('S')) {
cly = 1.0;
}
if (IsKeyDown('D')) {
clx = 1.0;
}
const int gamepad = player->gamepad;
clx += GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X);
cly += GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_Y);
crx += GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_X);
cry += GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_Y);
return (struct Motion){
.movement =
(struct Vec2D){
.x = clx,
.y = cly,
},
.shooting =
(struct Vec2D){
.x = crx,
.y = cry,
},
};
}
struct Motion PlanPlayerMoveSafely(struct Game *game, int player_index) {
struct Motion motion = PlanPlayerMove(game, player_index);
struct Vec2D pos;
pos = add(game->player[player_index].pos, ClampMotion(motion.movement));
if (IsFreePos(game, pos, player_index)) {
return motion;
}
struct Vec2D y_motion = (struct Vec2D){.x = 0, .y = motion.movement.y};
pos = add(game->player[player_index].pos, ClampMotion(y_motion));
if (IsFreePos(game, pos, player_index)) {
return (struct Motion){
.movement = y_motion,
.shooting = motion.shooting,
};
}
struct Vec2D x_motion = (struct Vec2D){.x = motion.movement.x, .y = 0};
pos = add(game->player[player_index].pos, ClampMotion(x_motion));
if (IsFreePos(game, pos, player_index)) {
return (struct Motion){
.movement = x_motion,
.shooting = motion.shooting,
};
}
/* no where to move! */
return (struct Motion){
.movement = origin,
.shooting = motion.shooting,
};
}
void MovePlayer(struct Game *game, int player_index) {
struct Player *player = &game->player[player_index];
/* get the player's desired directions */
struct Motion motion = PlanPlayerMoveSafely(game, player_index);
/* clean up the player's supplied directions */
motion.movement = ClampMotion(motion.movement);
motion.shooting = clamp_length(motion.shooting, 0.0, 0.5);
/* apply the desired motions to the world */
player->pos.x += motion.movement.x;
player->pos.y += motion.movement.y;
if (fabs(motion.shooting.x) > gamepad_fudge ||
fabs(motion.shooting.y) > gamepad_fudge) {
AddBullet(game, player_index,
player->pos.x + motion.movement.x + motion.shooting.x,
player->pos.y + motion.movement.y + motion.shooting.y,
#if INCLUDE_MOVEMENT_IN_SHOOTING
motion.movement.x +
#endif
motion.shooting.x,
#if INCLUDE_MOVEMENT_IN_SHOOTING
motion.movement.y +
#endif
motion.shooting.y);
}
/* clamp the player's position to the world coordinates */
player->pos.x = max(0, min(player->pos.x, gameScreenWidth - 1));
player->pos.y = max(0, min(player->pos.y, gameScreenHeight - 1));
}
void UpdateGameState(struct State *state) {
struct Game *game = state->game;
if (state->menu_visible) {
if (IsGamepadButtonPressed(GAMEPAD_PLAYER1, GAMEPAD_BUTTON_MIDDLE) ||
IsGamepadButtonPressed(GAMEPAD_PLAYER2, GAMEPAD_BUTTON_MIDDLE) ||
IsKeyPressed(KEY_ENTER)) {
InitGame(game, 1, 2);
state->menu_visible = false;
}
}
game->frame_count++;
MoveBullets(state);
for (int i = 0; i < GameGetPlayerCount(game); i++) {
MovePlayer(game, i);
}
}
float alpha(float x, float y) {
return x / y;
}
void DrawGame(struct Game *game) {
ClearBackground(BLACK);
for (int i = 0; i < game->score; i++) {
DrawPixel(i % gameScreenWidth, i / gameScreenWidth,
(Color){0xff, 0xff, 0x00, 0xff});
}
for (struct Bullet *bullet = game->bullets; bullet != NULL;
bullet = bullet->next) {
if (bullet->dead) {
continue;
}
DrawPixel(bullet->pos.x, bullet->pos.y,
IsBulletArmed(bullet, -1)
? bullet_colors[bullet->player]
: Fade(wall_color, Lerp(0.5, 1.0,
alpha(bullet->alive_until_frame -
game->frame_count,
BULLET_MAX_AGE))));
}
for (int i = 0; i < GameGetPlayerCount(game); i++) {
struct Player *player = &game->player[i];
DrawPixel(player->pos.x, player->pos.y, player->color);
}
}
void InitColors() {
free(player_colors);
player_colors = malloc(sizeof(player_colors[0]) * PLAYER_MAX);
free(bullet_colors);
bullet_colors = malloc(sizeof(bullet_colors[0]) * (PLAYER_MAX + 1));
for (int i = 0; i < PLAYER_MAX; i++) {
player_colors[i] = ColorFromHSV(
(struct Vector3){(float)i * 360.0 / (float)PLAYER_MAX, 0.85, 0.85});
bullet_colors[i] = ColorFromHSV(
(struct Vector3){(float)i * 360.0 / (float)PLAYER_MAX, 0.25, 0.5});
}
bullet_colors[PLAYER_MAX] = ColorFromHSV((struct Vector3){0, 0.0, 0.25});
}
void InitState(struct State *state) {
*state = (struct State){
.game = NewGame(0, MENU_DEMO_MONSTERS),
.menu_visible = true,
.game_paused = false,
.player_max_selection = 0,
};
}
void DrawMenu(const struct State *state) {
if (!state->menu_visible) {
return;
}
float inset = 1.0;
struct Rectangle r = {
.x = inset,
.y = inset,
.width = gameScreenWidth - inset * 2.0,
.height = gameScreenHeight - inset * 2.0,
};
DrawRectangleRounded(r, 0.1 /*roundness*/, 2 /*segments*/, Fade(GRAY, 0.7));
DrawText("bar-", 0, 0, 1, RED);
DrawText("rage", 6, 8, 1, RED);
}
int main(void) {
InitColors();
SetTraceLogLevel(LOG_WARNING);
SetConfigFlags(FLAG_FULLSCREEN_MODE);
InitWindow(windowWidth, windowHeight, "my 32x32 game/demo");
RenderTexture2D target = LoadRenderTexture(gameScreenWidth, gameScreenHeight);
SetTextureFilter(target.texture,
FILTER_POINT);
SetTargetFPS(60);
//--------------------------------------------------------------------------------------
struct State state;
InitState(&state);
while (!WindowShouldClose()) {
if (!state.game_paused) {
if (IsKeyPressed('P')) {
state.game_paused = true;
} else {
UpdateGameState(&state);
}
} else if (IsKeyPressed('P')) {
state.game_paused = false;
}
float scale = min((float)GetScreenWidth() / gameScreenWidth,
(float)GetScreenHeight() / gameScreenHeight) *
extra_scale;
BeginDrawing();
ClearBackground(BLACK);
BeginTextureMode(target);
DrawGame(state.game);
DrawMenu(&state);
EndTextureMode();
DrawTexturePro(target.texture,
(Rectangle){0.0f, 0.0f, (float)target.texture.width,
(float)-target.texture.height},
(Rectangle){0, 0, (float)gameScreenWidth * scale,
(float)gameScreenHeight * scale},
(Vector2){0, 0}, 0.0f, WHITE);
EndDrawing();
}
UnloadRenderTexture(target);
CloseWindow();
return EXIT_SUCCESS;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment