Skip to content

Instantly share code, notes, and snippets.

@FormerlyZeroCool
Created February 5, 2023 08:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FormerlyZeroCool/a71ee85c13660440e3e0dcb68d76cc54 to your computer and use it in GitHub Desktop.
Save FormerlyZeroCool/a71ee85c13660440e3e0dcb68d76cc54 to your computer and use it in GitHub Desktop.
Conway's game of life implemented with SDL(with makefile for macos)
#include <vector>
#include <array>
#include <exception>
#include <string>
#include <iostream>
#include <cmath>
#include <random>
#include <thread>
#include <mutex>
#include <SDL2/SDL.h>
template <typename P1>
P1 clamp(P1 val, P1 min, P1 max)
{
return std::min(std::max(min, val), max);
}
class ViewTransformation {
public:
float x_scale;
float y_scale;
float y_translation;
float x_translation;
float x_min;
float x_max;
float deltaX;
float y_min;
float y_max;
float deltaY;
std::array<float, 2> velocity;
std::array<float, 2> acceleration;
ViewTransformation(float x_scale, float y_scale, float x_translation, float y_translation): velocity {0}, acceleration {0}
{
this->x_scale = x_scale;
this->y_scale = y_scale;
this->x_translation = x_translation;
this->y_translation = y_translation;
recalc();
}
SDL_Rect SDL_view()
{
return {(int32_t)(x_min), (int32_t)(y_min), (int32_t)(deltaX), (int32_t)(deltaY) };
}
bool compare(ViewTransformation& target_bounds)
{
return target_bounds.x_scale == this->x_scale && target_bounds.x_translation == this->x_translation &&
target_bounds.y_scale == this->y_scale && target_bounds.y_translation == this->y_translation;
}
void update_state(float delta_time)
{
const float mult = delta_time / 1000;
this->x_translation = clamp(this->x_translation + this->velocity[0] * mult, -std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
this->y_translation = clamp(this->y_translation + this->velocity[1] * mult, -std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
const float velx_bounds = this->deltaX * 10;
const float vely_bounds = this->deltaY * 10;
this->velocity[0] = clamp(this->velocity[0] + this->acceleration[0] * mult, -velx_bounds, velx_bounds);
this->velocity[1] = clamp(this->velocity[1] + this->acceleration[1] * mult, -vely_bounds, vely_bounds);
this->recalc(this->x_scale, this->y_scale, this->x_translation, this->y_translation);
}
void stop_motion()
{
this->velocity[0] = 0;
this->velocity[1] = 0;
this->acceleration[0] = 0;
this->acceleration[1] = 0;
}
void recalc(float x_scale, float y_scale, float x_translation, float y_translation)
{
this->x_scale = x_scale;
this->y_scale = y_scale;
this->x_translation = x_translation;
this->y_translation = y_translation;
this->x_min = this->x_translation - 1 / this->x_scale;
this->x_max = this->x_translation + 1 / this->x_scale;
this->deltaX = this->x_max - this->x_min;
this->y_min = this->y_translation - 1 / this->y_scale;
this->y_max = this->y_translation + 1 / this->y_scale;
this->deltaY = this->y_max - this->y_min;
}
void recalc()
{
this->recalc(x_scale, y_scale, x_translation, y_translation);
}
ViewTransformation& copy(ViewTransformation& other)
{
this->recalc(other.x_scale, other.y_scale, other.x_translation, other.y_translation);
return *this;
}
};
class Window {
SDL_Window* window;
SDL_Renderer* sdl_renderer;
bool alive = true;
public:
int32_t width, height;
std::array<int32_t, 2> touch_pos;
std::array<int32_t, 2> delta_touch_pos;
Window(std::string name, int32_t width, int32_t height): width(width), height(height)
{
window = SDL_CreateWindow(name.c_str(), SDL_WINDOWPOS_CENTERED_DISPLAY(1), SDL_WINDOWPOS_CENTERED, width, height, 0);
if(window == nullptr)
throw std::string("Error could not create SDL window.");
sdl_renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if(sdl_renderer == nullptr)
throw std::string("Error could not initialize hardware accelerated renderer.");
}
SDL_Renderer& renderer()
{
return *sdl_renderer;
}
bool is_running()
{
return alive;
}
struct PollRec {
SDL_Event event;
bool success;
};
PollRec poll()
{
PollRec rec;
rec.success = SDL_PollEvent(&rec.event);
if(rec.event.type == SDL_QUIT)
{
rec.success = false;
alive = false;
}
else if(rec.event.type == SDL_MOUSEMOTION)
{
touch_pos[0] = rec.event.motion.x;
touch_pos[1] = rec.event.motion.y;
delta_touch_pos[0] = rec.event.motion.xrel;
delta_touch_pos[1] = rec.event.motion.yrel;
}
return rec;
}
SDL_Event wait_event()
{
SDL_Event event;
SDL_WaitEvent(&event);
if(event.type == SDL_QUIT)
{
alive = false;
}
else if(event.type == SDL_MOUSEMOTION)
{
touch_pos[0] = event.motion.x;
touch_pos[1] = event.motion.y;
delta_touch_pos[0] = event.motion.xrel;
delta_touch_pos[1] = event.motion.yrel;
}
return event;
}
};
class Conway {
private:
std::vector<bool> field_1;
std::vector<bool> field_2;
std::vector<bool>* current_field;
std::vector<bool>* back_buffer;
SDL_Texture* render_buf = nullptr;
ViewTransformation current_view;
Window& win;
public:
int32_t width, height;
std::mutex state_mutex;
std::mutex texture_mutex;
Conway(int32_t width, int32_t height, Window* win): width(width), height(height), current_view(2. / win->width, 2. / win->height, win->width >> 1, win->height >> 1),
win(*win)
{
if(win)
this->render_buf = SDL_CreateTexture(&win->renderer(), SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, width, height);
field_1.reserve(width * height);
field_2.reserve(width * height);
for(int64_t i = 0; i < width * height; i++)
{
field_1.push_back(false);
field_2.push_back(false);
}
this->current_field = &field_1;
back_buffer = &field_1;
}
~Conway()
{
SDL_DestroyTexture(this->render_buf);
}
Conway(Conway&) = delete;
Conway(Conway&&) = delete;
SDL_Texture& texture()
{
return *render_buf;
}
SDL_Rect get_view()
{
this->current_view.recalc();
return this->current_view.SDL_view();
}
void bring_to_life(std::array<int32_t, 2>& touch)
{
field()[touch[0] + touch[1] * width] = true;
}
std::array<float, 2> screen_to_world(std::array<int32_t, 2> coords)
{
return {(1.0f * coords[0] - current_view.x_min) / current_view.deltaX * width,
(-1.0f * coords[1] + current_view.y_min) / current_view.deltaY * height};
}
std::array<float, 2> world_to_screen(std::array<float, 2> coords)
{
return {coords[0] / width * win.width,
coords[1] / height * win.height};
}
void bring_to_life(std::array<int32_t, 2> touch, std::array<int32_t, 2> delta)
{
const auto world_touch = screen_to_world({touch[0], touch[1]});
delta[0] = 1.0 * delta[0] * width / win.width;
delta[1] = 1.0 * delta[1] * height / win.height;
delta[1] *= -1;
float error = 0;
const auto minx = std::min(world_touch[0], world_touch[0] + delta[0]);
const auto maxx = std::max(world_touch[0], world_touch[0] + delta[0]) + 1;
const auto miny = std::min(world_touch[1], world_touch[1] + delta[1]);
const auto maxy = std::max(world_touch[1], world_touch[1] + delta[1]) + 1;
if(abs(delta[0]) > abs(delta[1]))
{
const float m = delta[0] ? 1.0f * delta[1] / delta[0] : 0;
int32_t y = world_touch[1];
for(int32_t x = minx; x < maxx; x++)
{
y += (error > 0.5) - (error < -0.5);
error -= (error > 0.5) - (error < -0.5);
set_place((-y * width) + (x), true);
error += m;
}
}
else
{
const float m = delta[1] ? 1.0f * delta[0] / delta[1] : 0;
int32_t x = world_touch[0];
for(int32_t y = miny; y < maxy; y++)
{
x += (error > 0.5) - (error < -0.5);
error -= (error > 0.5) - (error < -0.5);
set_place((-y * width) + (x), true);
error += m;
}
}
}
bool is_alive(const std::vector<bool>& field, int64_t index) const
{
int64_t sum_on = 0;
const int32_t row1 = index - width - 1;
const int32_t row2 = index - 1;
const int32_t row3 = index + width - 1;
sum_on += field[row1];
sum_on += field[row1 + 1];
sum_on += field[row1 + 2];
sum_on += field[row2];
sum_on += field[row2 + 2];
sum_on += field[row3];
sum_on += field[row3 + 1];
sum_on += field[row3 + 2];
return (field[index] & (sum_on == 2)) | (sum_on == 3);
}
void tick()
{
int64_t i = width * 2;
std::vector<bool>& other = current_field == &field_1 ? field_2 : field_1;
for(; i < width * height - width * 2; i++)
{
other[i] = is_alive(*current_field, i);
}
back_buffer = current_field;
current_field = &other;
}
bool operator[](size_t index)
{
if(index < field().size())
return (field()[index]);
else
throw std::string("Error accessing outside bounds" + std::to_string((int64_t) index) + "\n");
}
void set_place(size_t index, bool value)
{
if(index < field().size())
field()[index] = value;
//else
// std::cerr<<std::string("Error accessing outside bounds" + std::to_string((int64_t) index));
}
std::vector<bool>& field() const
{
return *current_field;
}
std::vector<bool>& field_back() const
{
return *back_buffer;
}
void randomize()
{
for(uint64_t i = 0; i < field().size(); i++)
{
srand(i);
field()[i] = random() % 100 > 60;
}
}
void render_to_texture()
{
int32_t pitch = 0;
int32_t *lockedPixels = nullptr;
if(!SDL_LockTexture(render_buf, NULL, (void**)&lockedPixels, &pitch)) {
// lockedPixels now points to driver-provided memory that we can write to that the driver will then copy to texture
//FastCopy(uiPixels, lockedPixels, pitch); // can't ignore pitch, it might be wider than UI_WIDTH
//we might just try to ignore pitch
uint64_t i = 0;
const uint64_t remainder = field_back().size() % 8;
for(uint64_t i = 0; i < field_back().size() - remainder;)
{
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
lockedPixels[i++] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
}
for(uint64_t i = 0; i < remainder; i++)
{
lockedPixels[i] = field_back()[i] ? 0xFFFFFFFF : 0xFF0000FF;
}
SDL_UnlockTexture(render_buf);
}
}
void translate_view(float x, float y)
{
current_view.x_translation += x;
current_view.y_translation += y;
current_view.recalc();
}
void set_scale(float x_scale, float y_scale, std::array<int32_t, 2> keep_in_place)
{
const auto old_worldPos = this->screen_to_world(keep_in_place);
this->current_view.x_scale += x_scale * .1 / current_view.deltaX;
this->current_view.x_scale = clamp(this->current_view.x_scale, 1.f / win.width / 10, 5.f);
this->current_view.y_scale += y_scale * .1 / current_view.deltaY;
this->current_view.y_scale = clamp(this->current_view.y_scale, 1.f / win.height / 10, 5.f);
this->current_view.recalc();
const auto new_worldPos = this->screen_to_world(keep_in_place);
const auto screen_deltas = world_to_screen({(new_worldPos[0] - old_worldPos[0]), (new_worldPos[1] - old_worldPos[1])});
this->current_view.x_translation += (screen_deltas[0]);
this->current_view.y_translation -= (screen_deltas[1]);
}
};
void tick(Conway* con)
{
while(true)
{
con->tick();
//SDL_Delay(5);
}
}
void render_loop(Conway* con, Window* win)
{
while(true)
{
{
std::lock_guard(con->state_mutex);
con->render_to_texture();
SDL_RenderClear(&win->renderer());
SDL_Rect view_port = con->get_view();
SDL_RenderCopy(&win->renderer(), &con->texture(), nullptr, &view_port);
// Update the screen
SDL_RenderPresent(&win->renderer());
}
SDL_Delay(30);
}
}
void event_loop(Conway* con, Window* win, bool* mouse_down)
{
while(true)
{
SDL_Event event = win->wait_event();
std::lock_guard(con->state_mutex);
if(!win->is_running())
return;
if(event.type == SDL_KEYDOWN)
{
switch(event.key.keysym.sym)
{
case(SDLK_a):
con->randomize();
break;
}
}
else if(event.type == SDL_MOUSEMOTION)
{
if(!*mouse_down)
{
con->bring_to_life(win->touch_pos, win->delta_touch_pos);
}
else
{
con->translate_view(win->delta_touch_pos[0], win->delta_touch_pos[1]);
}
}
else if(event.type == SDL_MOUSEBUTTONDOWN)
{
*mouse_down = true;
}
else if(event.type == SDL_MOUSEBUTTONUP)
{
*mouse_down = false;
}
else if(event.type == SDL_MOUSEWHEEL)
{
con->set_scale(event.wheel.preciseY, event.wheel.preciseY, win->touch_pos);
}
}
}
int main()
{
const uint32_t dim = 1000;
const uint32_t win_dim = 750;
Window win("Main", win_dim, win_dim);
Conway con(dim, dim, &win);
bool mouse_down = false;
auto logic_thread = std::thread(tick, &con);
auto render_thread = std::thread(render_loop, &con, &win);
event_loop(&con, &win, &mouse_down);
return 0;
}
#
# Makefile for conway/SDL2
#
CC = g++
CXX = g++
INCLUDE = -I/usr/local/include/SDL
CFLAGS =
CXXFLAGS = -O3 -std=c++2a $(INCLUDE)
LDFLAGS = -L/usr/local/lib -lSDLmain -lSDL2 -Wl,-framework,Cocoa
default: conway
conway: conway.cpp
g++ -o $@ $@.cpp -O3 $(LDFLAGS) $(CXXFLAGS)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment