Last active
May 4, 2023 04:36
-
-
Save cellularmitosis/3249c675314c97f97e60ea6546f21354 to your computer and use it in GitHub Desktop.
Simple gravity game in C and SDL 1.2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A simple "gravity" game written in C / SDL 1.2. | |
// Copyright 2023 Jason Pepas | |
// Released under the terms of the MIT license | |
// See https://opensource.org/licenses/MIT | |
// Code style: | |
// - camelCase for identifiers. | |
// - 'p' suffix used for pointers. | |
// - 'g_' prefix used for global variables. | |
// The code structure is modelled after PICO-8's init() / update() / draw(). | |
#include <stdint.h> // uint8_t | |
#include <assert.h> // assert | |
#include <stdbool.h> // true | |
#include <stdlib.h> // exit, srand, rand | |
#include <time.h> // time | |
#include <math.h> // sqrt | |
#define unreachable assert(false);exit(99); | |
#ifdef __APPLE__ | |
#include <SDL.h> | |
#else | |
#include <SDL/SDL.h> | |
#endif | |
// MARK: - Types | |
typedef uint32_t Color; | |
struct _Position2D { | |
float x; | |
float y; | |
}; | |
typedef struct _Position2D Position2D; | |
struct _Velocity2D { | |
float x; | |
float y; | |
}; | |
typedef struct _Velocity2D Velocity2D; | |
struct _Planet { | |
Position2D center; | |
float radius; | |
float mass; | |
Color color; | |
}; | |
typedef struct _Planet Planet; | |
struct _Comet { | |
Position2D center; | |
float radius; | |
float mass; | |
Velocity2D velocity; | |
Color color; | |
}; | |
typedef struct _Comet Comet; | |
// The game state. | |
struct _State { | |
Comet comet; | |
Planet planet; | |
SDL_Surface* screenp; | |
int screenWidth; | |
int screenHeight; | |
uint32_t framePeriod; | |
uint32_t lastFrame; | |
Color bgColor; | |
}; | |
typedef struct _State State; | |
// MARK: - Colors | |
Color g_black; | |
Color g_white; | |
Color g_red; | |
// Make the specified RGB color. | |
Color makeColor(State* statep, uint8_t r, uint8_t g, uint8_t b) { | |
return SDL_MapRGB(statep->screenp->format, r, g, b); | |
} | |
// Generate a random color. | |
Color randomColor(State* statep) { | |
uint8_t r = rand() & 0xFF; | |
uint8_t g = rand() & 0xFF; | |
uint8_t b = rand() & 0xFF; | |
return SDL_MapRGB(statep->screenp->format, r, g, b); | |
} | |
// MARK: - Drawing | |
// Draw the comet. | |
void drawComet(State* statep) { | |
int16_t x = statep->comet.center.x - statep->comet.radius; | |
int16_t y = statep->comet.center.y - statep->comet.radius; | |
uint16_t w = statep->comet.radius * 2; | |
uint16_t h = statep->comet.radius * 2; | |
SDL_Rect rect = {x, y, w, h}; | |
SDL_FillRect(statep->screenp, &rect, statep->comet.color); | |
} | |
bool planetIsHidden(State* statep); | |
// Draw the planet. | |
void drawPlanet(State* statep) { | |
if (planetIsHidden(statep)) { | |
return; | |
} | |
int16_t x = statep->planet.center.x - statep->planet.radius; | |
int16_t y = statep->planet.center.y - statep->planet.radius; | |
uint16_t w = statep->planet.radius * 2; | |
uint16_t h = statep->planet.radius * 2; | |
SDL_Rect rect = {x, y, w, h}; | |
SDL_FillRect(statep->screenp, &rect, statep->planet.color); | |
} | |
// Draw the background. | |
void drawBG(State* statep) { | |
int16_t x = 0; | |
int16_t y = 0; | |
uint16_t w = statep->screenWidth; | |
uint16_t h = statep->screenHeight; | |
SDL_Rect rect = {x, y, w, h}; | |
SDL_FillRect(statep->screenp, &rect, statep->bgColor); | |
} | |
// Perform all drawing. Called once per frame. | |
void draw(State* statep) { | |
drawBG(statep); | |
drawPlanet(statep); | |
drawComet(statep); | |
SDL_Flip(statep->screenp); | |
} | |
// MARK: - State | |
// FIXME make this random. | |
void initRandomComet(State* statep) { | |
statep->comet.mass = 1.0; | |
statep->comet.radius = 10; | |
statep->comet.color = g_white; | |
statep->comet.center.x = 1; | |
statep->comet.center.y = 100; | |
statep->comet.velocity.x = 2; | |
statep->comet.velocity.y = 0; | |
} | |
// Place a planet at the given coords. | |
void initPlanet(State* statep, int16_t x, int16_t y) { | |
statep->planet.center.x = x; | |
statep->planet.center.y = y; | |
statep->planet.radius = 40; | |
statep->planet.mass = 100000.0; | |
statep->planet.color = g_red; | |
} | |
// Hide the planet and remove it from the gravity calculation. | |
void hidePlanet(State* statep) { | |
statep->planet.center.x = -1; | |
} | |
// Is the planet hidden? | |
bool planetIsHidden(State* statep) { | |
return statep->planet.center.x == -1; | |
} | |
// The distance between two points. | |
float distance2D(Position2D a, Position2D b) { | |
// https://en.wikipedia.org/wiki/Pythagorean_theorem | |
float dx = a.x - b.x; | |
float dy = a.y - b.y; | |
return sqrt((dx * dx) + (dy * dy)); | |
} | |
// The angle made by two points. | |
float radians2D(Position2D a, Position2D b) { | |
// Thanks to https://math.stackexchange.com/a/1201367 | |
return atan2f(a.y - b.y, a.x - b.x); | |
} | |
// Update the comet's position. | |
void moveComet(State* statep) { | |
if (!planetIsHidden(statep)) { | |
// See https://en.wikipedia.org/wiki/Newton%27s_law_of_universal_gravitation | |
// F = G (m1 * m2) / (r * r) | |
// F = m * A | |
// So: | |
// A = G m2 / (r * r) | |
// The below math behaves similarly. | |
float r = distance2D(statep->comet.center, statep->planet.center); | |
float period = (float)(statep->framePeriod) / 1000.0; | |
float deltaV = statep->planet.mass / (r * r) * period; | |
float radians = radians2D(statep->planet.center, statep->comet.center); | |
float deltaVy = deltaV * sinf(radians); | |
float deltaVx = deltaV * cosf(radians); | |
statep->comet.velocity.x += deltaVx; | |
statep->comet.velocity.y += deltaVy; | |
} | |
statep->comet.center.x += statep->comet.velocity.x; | |
statep->comet.center.y += statep->comet.velocity.y; | |
} | |
// Did the mouse click land inside the planet? | |
bool pointIntersectsPlanet(State* statep, int16_t x, int16_t y) { | |
Planet p = statep->planet; | |
float px1 = p.center.x - p.radius; | |
float px2 = p.center.x + p.radius; | |
float py1 = p.center.y - p.radius; | |
float py2 = p.center.y + p.radius; | |
return (x > px1 && x < px2 && y > py1 && y < py2); | |
} | |
// Mouse click handler. | |
void didClick(State* statep, int16_t x, int16_t y) { | |
if (!planetIsHidden(statep) && pointIntersectsPlanet(statep, x, y)) { | |
hidePlanet(statep); | |
} else { | |
initPlanet(statep, x, y); | |
} | |
} | |
// Is the comet off-screen? | |
bool cometIsOutOfBounds(State* statep) { | |
Position2D c = statep->comet.center; | |
return (c.x < 0) || (c.y < 0) || (c.x > statep->screenWidth) || (c.y > statep->screenHeight); | |
} | |
// Restart: start a new game. | |
void restart(State* statep) { | |
hidePlanet(statep); | |
initRandomComet(statep); | |
} | |
// Exit the game (terminate the process). | |
void quit(int status) { | |
SDL_Quit(); | |
exit(status); | |
} | |
// Update the game state. Called once per frame. | |
void update(State* statep) { | |
SDL_Event event; | |
while (SDL_PollEvent(&event) == 1) { | |
if (event.type == SDL_QUIT) { | |
// the window was closed. | |
quit(0); | |
} else if (event.type == SDL_KEYDOWN) { | |
SDLKey k = event.key.keysym.sym; | |
// check if we need to quit. | |
if (k == SDLK_ESCAPE || k == SDLK_q) { | |
quit(0); | |
} | |
} else if (event.type == SDL_MOUSEBUTTONDOWN) { | |
if (event.button.button == SDL_BUTTON_LEFT) { | |
didClick(statep, event.button.x, event.button.y); | |
continue; | |
} | |
} | |
continue; | |
} | |
// if the comet leaves the screen, restart. | |
if (cometIsOutOfBounds(statep)) { | |
restart(statep); | |
} else { | |
moveComet(statep); | |
} | |
} | |
// Perform all initialization. | |
void init(State* statep) { | |
srand(time(NULL)); | |
int ret = SDL_Init(SDL_INIT_VIDEO); | |
assert(ret == 0); | |
statep->screenWidth = 1024; | |
statep->screenHeight = 768; | |
int bpp = 0; // use current bits per pixel. | |
uint32_t flags = SDL_HWSURFACE; | |
statep->screenp = SDL_SetVideoMode(statep->screenWidth, statep->screenHeight, bpp, flags); | |
assert(statep->screenp != NULL); | |
g_black = makeColor(statep, 0, 0, 0); | |
g_white = makeColor(statep, 255, 255, 255); | |
g_red = makeColor(statep, 255, 0, 0); | |
statep->bgColor = g_black; | |
statep->framePeriod = 17; // in milliseconds | |
statep->lastFrame = 0; | |
restart(statep); | |
} | |
// MARK: - Main | |
// The process entry point. | |
int main(int argc, char** argv) { | |
State state; | |
init(&state); | |
while (true) { | |
uint32_t ticks = SDL_GetTicks(); | |
uint32_t elapsed = ticks - state.lastFrame; | |
if (elapsed >= state.framePeriod) { | |
update(&state); | |
draw(&state); | |
if (elapsed > state.framePeriod * 2) { | |
// catch up. | |
state.lastFrame = ticks; | |
} else { | |
state.lastFrame += state.framePeriod; | |
} | |
} else { | |
uint32_t remaining = state.framePeriod - elapsed; | |
SDL_Delay(remaining); | |
} | |
continue; | |
} | |
return 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
SDL=$(shell sdl-config --cflags --libs) | |
default: gravity run | |
run: gravity | |
./gravity | |
gravity: gravity.c | |
gcc -std=c99 -Wall -Werror $(SDL) gravity.c -o gravity | |
web: gravity.html | |
gravity.html: | |
emcc -O3 -s ASYNCIFY gravity.c -o gravity.html | |
clean: | |
rm -f gravity gravity.js gravity.wasm gravity.html | |
.PHONY: default run web clean |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment