Created
February 5, 2023 08:02
-
-
Save FormerlyZeroCool/a71ee85c13660440e3e0dcb68d76cc54 to your computer and use it in GitHub Desktop.
Conway's game of life implemented with SDL(with makefile for macos)
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
#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; | |
} |
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
# | |
# 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