This commit is contained in:
anon 2024-12-21 14:08:37 +01:00
commit 5715f25bf2
20 changed files with 743 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.gdb_history
*.out

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
LDFLAGS += -lraylib -lGL -lm -lpthread -ldl -lrt -lX11
ifeq (${DEBUG}, 1)
CPPFLAGS += -DDEBUG
CFLAGS += -ggdb
endif
OUT := masses.out
${OUT}: source/main.cpp source/main_menu.cpp source/Sprite2D.c
g++ -o ${OUT} ${CPPFLAGS} ${CFLAGS} $+ ${LDFLAGS}
clean:
-rm ${OUT}

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# Masses
This is a dummy game for getting comfortable with raylib and trying out gamedev.
The player model is a reference to 'The Owl Who Was God' by James Thurber.
### Lacking:
+ the movement is a bit wacky
+ the "path finding" is buggy, but I like it that way
+ the """art""" is terrible
+ the music is copyrighted; i wanted wardrums speeding up based on the distance and player speed, but i had trouble finding appropriate sounds samples

BIN
resources/enemy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

BIN
resources/entity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

BIN
resources/obstackle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
resources/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
resources/road.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
resources/street.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

86
source/Effect.hpp Normal file
View File

@ -0,0 +1,86 @@
#ifndef EFFECT_HPP
#define EFFECT_HPP
class DeathEffect {
public:
class Toss {
public:
constexpr static double gravity = 98.0d * 4;
double last_update;
Vector2 starting_position;
Vector2 position;
Vector2 velocity;
Toss(Vector2 start_position) : starting_position(start_position) {
position = starting_position;
velocity.y = (rand() % 140) + 140;
velocity.x = (rand() % (75*2)) - 75;
last_update = GetTime();
}
int update(double t) {
if (position.y > starting_position.y) {
return 1;
}
const double dt = t - last_update;
Vector2 delta;
delta.x = velocity.x * dt;
delta.y = velocity.y * dt;
position.x += delta.x;
position.y -= delta.y;
velocity.y -= gravity * dt;
last_update = t;
return 0;
}
void display(void) {
DrawRectangle(
position.x - (4/2),
position.y - (4/2),
4,
4,
RED
);
}
};
Vector2 source;
std::vector<Toss> tosses;
bool is_done = false;
DeathEffect(Vector2 source_position) : source(source_position) {
int n = (rand() % 50) + 20;
tosses.reserve(n);
for (int i = 0; i < n; i++) {
tosses.emplace_back(
(Vector2) {
.x = source.x + ((rand() % 6) - 3),
.y = source.y + ((rand() % 10) - 5),
}
);
}
}
void update(void) {
bool are_tosses_done = true;
for (auto &t : tosses) {
are_tosses_done &= t.update(GetTime());
}
is_done = are_tosses_done;
}
void display(void) {
for (auto &t : tosses) {
t.display();
}
}
};
#endif

79
source/Entity.hpp Normal file
View File

@ -0,0 +1,79 @@
#include <stdio.h>
#include <stdlib.h>
class Entity : public Rectangle {
public:
Color color = BLACK;
Sprite2D sprite;
int frame = 0;
float x_velocity, y_velocity;
Entity(void) {
this->width = 20;
this->height = 20;
this->sprite.texture = &entity_texture;
this->sprite.width = this->sprite.texture->width;
this->sprite.height = this->sprite.texture->height;
}
void draw(void) {
DrawSprite2D(
this->sprite,
frame,
this->x,
this->y,
WHITE
);
}
};
Entity player; // entity is initialized before the texture is loaded
class Enemy : public Entity {
public:
bool is_agrod = false;
Enemy(void) {
this->x = 800 + (rand() % 500);
if (rand() % 2) {
this->y = SIDE_WALK1 + (rand() % SIDE_WALK1_W);
} else {
this->y = SIDE_WALK2 + (rand() % SIDE_WALK2_W);
}
this->color = RED;
this->sprite.texture = &enemy_texture;
this->sprite.width = this->sprite.texture->width;
this->sprite.height = this->sprite.texture->height;
}
};
class Obstacle : public Entity {
public:
static constexpr float speed = 1.0f;
Obstacle(void) {
this->width = 85;
this->height = 45;
this->x = -100;
this->y = -100;
this->color = BROWN;
}
void regen(void) {
const int y_area = GAME_HEIGHT - (SIDE_WALK1 + SIDE_WALK1_W) - SIDE_WALK2_W;
const double unit = (double)1 / (double)radical_number_generator.range_max;
this->x = GAME_WIDTH + 10;
this->y = (SIDE_WALK1 + SIDE_WALK1_W/2)
+ y_area * (unit * radical_number_generator())
;
this->sprite.texture = &obstacle_texture;
this->sprite.width = this->sprite.texture->width;
this->sprite.height = this->sprite.texture->height;
}
};

25
source/Sprite2D.c Normal file
View File

@ -0,0 +1,25 @@
#include "Sprite2D.h"
void DrawSprite2D(const Sprite2D sprite, int frame, int posX, int posY, Color tint) {
DrawTexturePro(
*sprite.texture,
(Rectangle) {
.x = 0,
.y = (float)(frame * sprite.height),
.width = (float)sprite.width,
.height = (float)sprite.height,
},
(Rectangle) {
.x = (float)posX,
.y = (float)posY,
.width = (float)sprite.width,
.height = (float)sprite.height,
},
(Vector2) {
.x = 0,
.y = 0,
},
0.0f,
tint
);
}

14
source/Sprite2D.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef SPRITE2D_H
#define SPRITE2D_H
#include "raylib.h"
typedef struct Sprite2D {
Texture2D * texture;
int width;
int height;
} Sprite2D;
extern void DrawSprite2D(const Sprite2D sprite, int frame, int posX, int posY, Color tint);
#endif

38
source/death_manager.h Normal file
View File

@ -0,0 +1,38 @@
static std::list<DeathEffect*> death_effects;
void reset_deaths(void) {
death_effects.clear();
}
void add_death(Vector2 position) {
death_effects.push_back(new DeathEffect(position));
}
void update_deaths(void) {
for (auto &d : death_effects) {
d->update();
}
std::vector<decltype(death_effects.begin())> to_erase;
for (auto i = death_effects.begin(); i != death_effects.end(); i++) {
(*i)->source.x -= v*v;
for (auto &t : (*i)->tosses) {
t.position.x -= v*v;
}
if ((*i)->source.x < -100) {
to_erase.push_back(i);
}
}
for (auto &e : to_erase) {
delete *e;
death_effects.erase(e);
}
}
void display_deaths(void) {
for (auto &d : death_effects) {
d->display();
}
}

359
source/main.cpp Normal file
View File

@ -0,0 +1,359 @@
#include <math.h>
#include <time.h>
#include <vector>
#include <list>
#include <algorithm>
#include "raylib.h"
#include "Sprite2D.h"
Texture2D background;
Texture2D entity_texture;
Texture2D player_texture;
Texture2D enemy_texture;
Texture2D obstacle_texture;
Sound background_music;
const int GAME_WIDTH = 800;
const int GAME_HEIGHT = GAME_WIDTH * (3.0f / 5.0f);
const int SIDE_WALK1 = 40;
const int SIDE_WALK1_W = 50;
const int SIDE_WALK2_W = SIDE_WALK1_W;
const int SIDE_WALK2 = GAME_HEIGHT - SIDE_WALK2_W;
const int AGRO_BUFFER_DISTANCE = 10;
const double RIGHT_MARGIN = (3.0f / 5.0f);
float v;
#include "util.h"
#include "my_random.hpp"
#include "main_menu.h"
#include "Effect.hpp"
#include "Entity.hpp"
#include "death_manager.h"
using namespace std;
float absolute_x;
Spawner enemy_spawner = (Spawner) {
.min_distance = 400,
.denominator = 10,
};
Spawner obstacle_spawner = (Spawner) {
.min_distance = 1200,
.denominator = 1000,
};
const float max_v = 3.4f;
list<Enemy> enemies;
vector<Enemy*> ordered_enemies;
Obstacle obstacle;
const float MOVEMENT_SPEED = 3.2;
float move_player(bool left, bool right, bool up, bool down) {
static const float mod = MOVEMENT_SPEED;
float x_mod = 0;
float y_mod = 0;
if (left) { x_mod -= mod; }
if (right) { x_mod += mod; }
if (up) { y_mod -= mod; }
if (down) { y_mod += mod; }
if (player.x + x_mod > 0
&& player.x + x_mod < GAME_WIDTH * RIGHT_MARGIN) {
player.x += x_mod;
}
if (player.y + y_mod > SIDE_WALK1
&& player.y < GAME_HEIGHT) {
player.y += y_mod;
}
return x_mod;
}
void update_scroll(float mod) {
if (mod > 0
&& v + 0.01f < max_v) {
v += 0.01f;
} else
if (v - 0.1f > 0.0f) {
v -= 0.1f;
} else {
v = 0;
}
absolute_x += v*v;
}
int game_loop(void) {
// Init
enum {
GAME_RUNNING,
GAME_OVER,
} game_state = GAME_RUNNING;
double gameover_time;
enemy_spawner.reset();
obstacle_spawner.reset();
reset_deaths();
enemies.clear();
ordered_enemies.clear();
obstacle.regen();
player.x = 100;
player.y = 100;
absolute_x = 0;
v = 0.0f;
// Work
PlaySound(background_music);
float mod = 0;
while (!WindowShouldClose()) {
input:
if (game_state == GAME_OVER) {
if ((GetTime() - gameover_time) > 0.5
&& GetKeyPressed() != KEY_NULL) {
return 0;
}
mod = 0;
goto simulate;
}
mod = move_player(
IsKeyDown(KEY_A) | IsKeyDown(KEY_LEFT) | IsKeyDown(KEY_H),
IsKeyDown(KEY_D) | IsKeyDown(KEY_RIGHT) | IsKeyDown(KEY_L),
IsKeyDown(KEY_W) | IsKeyDown(KEY_UP) | IsKeyDown(KEY_K),
IsKeyDown(KEY_S) | IsKeyDown(KEY_DOWN) | IsKeyDown(KEY_J)
);
simulate:
// Map movement
update_scroll(mod);
// Player animation
if (((int)absolute_x / 10) % 2) {
player.frame = player.frame xor 0x01;
}
// Spawn
if (enemy_spawner.blaze(absolute_x)) {
enemies.emplace_back();
}
if (obstacle_spawner.blaze(absolute_x)) {
obstacle.regen();
}
// Obstacle movement
obstacle.x -= v*v + obstacle.speed;
// Player death
if (CheckCollisionRecs((Rectangle)obstacle, (Rectangle)player)) {
add_death((Vector2) { player.x, player.y });
gameover_time = GetTime();
game_state = GAME_OVER;
}
// Enemy Death
vector<decltype(enemies.begin())> to_erase;
for (auto e = enemies.begin(); e != enemies.end(); e++) {
if (CheckCollisionRecs((Rectangle)*e, (Rectangle)obstacle)) {
add_death((Vector2) { e->x, e->y });
to_erase.push_back(e);
}
}
for (auto e : to_erase) {
enemies.erase(e);
}
// Enemy movement
ordered_enemies.clear();
for (auto &e : enemies) {
ordered_enemies.push_back(&e);
}
sort(ordered_enemies.begin(), ordered_enemies.end(), [](Enemy * a, Enemy * b) {
return distance(a->x, a->y, player.x, player.y)
< distance(b->x, b->y, player.x, player.y);
});
for (auto &e : ordered_enemies) {
// non-agro
if (not e->is_agrod) {
e->x -= v*v;
if (e->x - AGRO_BUFFER_DISTANCE < player.x) {
e->is_agrod = true;
}
continue;
}
// murder player
if (CheckCollisionRecs((Rectangle)*e, (Rectangle)player)) {
add_death((Vector2) { player.x, player.y });
gameover_time = GetTime();
game_state = GAME_OVER;
}
// move
Rectangle would_be_rect = (Rectangle)*e;
float diff = player.y - e->y;
would_be_rect.y += diff * 0.05f;
float mod = -(v*v) + (MOVEMENT_SPEED*0.9f);
if (e->x + mod > 0) {
would_be_rect.x += mod;
}
would_be_rect.y += random_wiggle(64, 6);
would_be_rect.y += -abs(random_wiggle(32, 3));
bool would_collide_x = false;
bool would_collide_y = false;
for (const auto &r : enemies) {
if (would_collide_x
&& would_collide_y) {
break;
}
if (e == &r) { continue; }
if (CheckCollisionRecs(
(Rectangle) {
.x = would_be_rect.x,
.y = e->y,
.width = e->width,
.height = e->height,
},
(Rectangle)r
)
) {
would_collide_x = true;
}
if (CheckCollisionRecs(
(Rectangle) {
.x = e->x,
.y = would_be_rect.y,
.width = e->width,
.height = e->height,
},
(Rectangle)r
)
) {
would_collide_y = true;
}
}
if (not would_collide_x) {
e->x = would_be_rect.x;
}
if (not would_collide_y) {
e->y = would_be_rect.y;
}
}
update_deaths();
draw:
BeginDrawing();
ClearBackground(RAYWHITE);
// draw street
int background_one_start = - (int)absolute_x % background.width;
int background_two_start = background_one_start + background.width;
DrawTexture(background, background_one_start, 0, WHITE);
DrawTexture(background, background_two_start, 0, WHITE);
display_deaths();
// draw entities
obstacle.draw();
if (game_state == GAME_RUNNING) {
player.draw();
}
for (auto &e : enemies) {
e.draw();
}
#ifdef DEBUG
for (int i = 0; i < ordered_enemies.size(); i++) {
DrawText(
TextFormat("%d", i),
ordered_enemies[i]->x,
ordered_enemies[i]->y,
20,
BLUE
);
}
#endif
// draw HUD
#ifdef DEBUG
DrawText(TextFormat("d: %f", absolute_x), 10, 15, 20, BLACK);
DrawText(TextFormat("V: %f", v), 10, 40, 20, BLACK);
#endif
if (game_state == GAME_OVER) {
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color{128, 128, 128, 128});
DrawText("Game Over", 100, 100, 40, BLACK);
DrawText(
TextFormat(
"You lead the masses for %d meters.",
((int)absolute_x) / 10
),
200,
200,
20,
BLACK
);
}
EndDrawing();
}
return 1;
}
int main(void) {
// Init
srand(time(NULL));
InitWindow(GAME_WIDTH, GAME_HEIGHT, "Masses");
InitAudioDevice();
Image image = LoadImage("resources/street.png");
ImageResize(&image, GAME_WIDTH, GAME_HEIGHT);
background = LoadTextureFromImage(image);
UnloadImage(image);
player_texture = LoadTexture("resources/player.png");
player.sprite.texture = &player_texture;
player.sprite.width = player.sprite.texture->width;
player.sprite.height = player.sprite.texture->height / 2;
entity_texture = LoadTexture("resources/entity.png");
enemy_texture = LoadTexture("resources/enemy.png");
obstacle_texture = LoadTexture("resources/obstackle.png");
background_music = LoadSound("resources/From_Nothing_To_Zero_-_(Sybreed_song_reversed).mp3");
SetTargetFPS(60);
// Game
main_menu(); // hang until initial user input
while (not game_loop()) { ; }
CloseWindow();
return 0;
}

12
source/main_menu.cpp Normal file
View File

@ -0,0 +1,12 @@
#include "main_menu.h"
#include "raylib.h"
void main_menu(void) {
while (GetKeyPressed() == KEY_NULL) {
BeginDrawing();
ClearBackground(RAYWHITE);
DrawText("Start", 100, 100, 40, BLACK);
EndDrawing();
}
}

6
source/main_menu.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef MAIN_MENU_H
#define MAIN_MENU_H
extern void main_menu(void);
#endif

82
source/my_random.hpp Normal file
View File

@ -0,0 +1,82 @@
#ifndef MY_RANDOM_HPP
#define MY_RANDOM_HPP
#include <vector>
#include <random>
/* For spawning a truck, we would like to have a system where they mostly come from a lane.
* However, that would be too predictable.
* For this reason we implement our own distribution of probabilities,
* where they scew to both sides.
* Very roughly this:
* .. ..
* .-` `-.-` `-.
*/
class RadicalNumberGenerator {
public:
const int range_max = 100;
private:
std::mt19937 gen;
std::discrete_distribution<> dist;
/* To achieve the desired form,
* we use the sum of 2 normal distributions.
*/
double normal_probability_density_function(double x, double mean, double variance) {
return (1 / sqrt(2*M_PI*pow(variance, 2)))
* exp(-0.5 * (pow(x-mean, 2) / (2*pow(variance, 2))))
;
}
double radical_probability_density_function(double x) {
double variance = 6.8d;
return normal_probability_density_function(x, range_max * 0.2, variance)
+ normal_probability_density_function(x, range_max - (range_max * 0.2), variance);
}
public:
RadicalNumberGenerator(int seed) : gen(seed) {
std::vector<double> probabilities(range_max);
for (int i = 0; i < range_max; i++) {
probabilities[i] = std::max(
0.0,
radical_probability_density_function((double)i)
);
}
dist = std::discrete_distribution(probabilities.begin(), probabilities.end());
}
double operator()(void) {
return dist(gen);
}
} radical_number_generator(rand());
/* We would like to spawn enemies and trucks at semi-random intervals,
* without too much RNG involved.
*/
class Spawner {
public:
int last_blaze = 0;
int min_distance;
int denominator;
void reset() {
last_blaze = 0;
}
bool blaze(int current_distance) {
if ((current_distance - last_blaze) > min_distance
&& rand() % denominator) {
last_blaze = current_distance;
return true;
}
return false;
}
};
#endif

16
source/util.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef UNTIL_H
#define UNTIL_H
static
float distance(float x1, float y1, float x2, float y2) {
return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
}
int random_wiggle(int denominator, int radius) {
return (rand() % denominator)
? (rand() % (2*radius)) - radius
: 0
;
}
#endif