带有OpenGL的C ++(+17)
因此,我尝试了3-等角凸五边形网格。对我有用;)适用标准的生活游戏规则,但网格不是无限的-图像外部有边界单元。最初有30%的细胞存活。
这是网格的样子:
现场版:
蓝色细胞存活,白色细胞死亡。红细胞刚刚死亡,绿色刚刚诞生。请注意,图像中的伪像是gif压缩的结果,因此不喜欢10MB gifs :(。
静物:(+ 2)
振荡器T = 2,T = 3,T = 12:(+9)
振荡器T = 6,T = 7:(+6)
还有更多不同的振荡器...但是似乎网格对于船舶来说不够规则...
这没什么(没分),但是我喜欢它:
该代码是一团糟:)使用一些古老的固定OpenGL。否则,将GLEW,GLFW,GLM和ImageMagick用于gif导出。
/**
* Tile pattern generation is inspired by the code
* on http://www.jaapsch.net/tilings/
* It saved me a lot of thinkink (and debugging) - thank you, sir!
*/
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <FTGL/ftgl.h> //debug only
#include <ImageMagick-6/Magick++.h> //gif export
#include "glm/glm.hpp"
#include <iostream>
#include <array>
#include <vector>
#include <set>
#include <algorithm>
#include <unistd.h>
typedef glm::vec2 Point;
typedef glm::vec3 Color;
struct Tile {
enum State {ALIVE=0, DEAD, BORN, DIED, SIZE};
static const int VERTICES = 5;
static constexpr float SCALE = 0.13f;
static constexpr std::array<std::array<int, 7>, 18> DESC
{{
{{1, 0,0, 0,0,0, 0}},
{{0, 1,2, 0,2,1, 0}},
{{2, 2,3, 0,2,3, 1}},
{{1, 0,4, 0,0,1, 0}},
{{0, 1,2, 3,2,1, 0}},
{{2, 2,3, 3,2,3, 1}},
{{1, 0,4, 3,0,1, 0}},
{{0, 1,2, 6,2,1, 0}},
{{2, 2,3, 6,2,3, 1}},
{{1, 0,4, 6,0,1, 0}},
{{0, 1,2, 9,2,1, 0}},
{{2, 2,3, 9,2,3, 1}},
{{1, 0,4, 9,0,1, 0}},
{{0, 1,2,12,2,1, 0}},
{{2, 2,3,12,2,3, 1}},
{{1, 0,4,12,0,1, 0}},
{{0, 1,2,15,2,1, 0}},
{{2, 2,3,15,2,3, 1}}
}};
const int ID;
std::vector<Point> coords;
std::set<Tile*> neighbours;
State state;
State nextState;
Color color;
Tile() : ID(-1), state(DEAD), nextState(DEAD), color(1, 1, 1) {
const float ln = 0.6f;
const float h = ln * sqrt(3) / 2.f;
coords = {
Point(0.f, 0.f),
Point(ln, 0.f),
Point(ln*3/2.f,h),
Point(ln, h*4/3.f),
Point(ln/2.f, h)
};
for(auto &c : coords) {
c *= SCALE;
}
}
Tile(const int id, const std::vector<Point> coords_) :
ID(id), coords(coords_), state(DEAD), nextState(DEAD), color(1, 1, 1) {}
bool operator== (const Tile &other) const {
return ID == other.ID;
}
const Point & operator[] (const int i) const {
return coords[i];
}
void updateState() {
state = nextState;
}
/// returns "old" state
bool isDead() const {
return state == DEAD || state == DIED;
}
/// returns "old" state
bool isAlive() const {
return state == ALIVE || state == BORN;
}
void translate(const Point &p) {
for(auto &c : coords) {
c += p;
}
}
void rotate(const Point &p, const float angle) {
const float si = sin(angle);
const float co = cos(angle);
for(auto &c : coords) {
Point tmp = c - p;
c.x = tmp.x * co - tmp.y * si + p.x;
c.y = tmp.y * co + tmp.x * si + p.y;
}
}
void mirror(const float y2) {
for(auto &c : coords) {
c.y = y2 - (c.y - y2);
}
}
};
std::array<std::array<int, 7>, 18> constexpr Tile::DESC;
constexpr float Tile::SCALE;
class Game {
static const int CHANCE_TO_LIVE = 30; //% of cells initially alive
static const int dim = 4; //evil grid param
FTGLPixmapFont &font;
std::vector<Tile> tiles;
bool animate; //animate death/birth
bool debug; //show cell numbers (very slow)
bool exportGif; //save gif
bool run;
public:
Game(FTGLPixmapFont& font) : font(font), animate(false), debug(false), exportGif(false), run(false) {
//create the initial pattern
std::vector<Tile> init(18);
for(int i = 0; i < Tile::DESC.size(); ++i) {
auto &desc = Tile::DESC[i];
Tile &tile = init[i];
switch(desc[0]) { //just to check the grid
case 0: tile.color = Color(1, 1, 1);break;
case 1: tile.color = Color(1, 0.7, 0.7);break;
case 2: tile.color = Color(0.7, 0.7, 1);break;
}
if(desc[3] != i) {
const Tile &tile2 = init[desc[3]];
tile.translate(tile2[desc[4]] - tile[desc[1]]);
if(desc[6] != 0) {
float angleRad = getAngle(tile[desc[1]], tile[desc[2]]);
tile.rotate(tile[desc[1]], -angleRad);
tile.mirror(tile[desc[1]].y);
angleRad = getAngle(tile[desc[1]], tile2[desc[5]]);
tile.rotate(tile[desc[1]], angleRad);
}
else {
float angleRad = getAngle(tile[desc[1]], tile[desc[2]], tile2[desc[5]]);
tile.rotate(tile[desc[1]], angleRad);
}
}
}
const float offsets[4] {
init[2][8].x - init[8][9].x,
init[2][10].y - init[8][11].y,
init[8][12].x - init[14][13].x,
init[8][14].y - init[14][15].y
};
// create all the tiles
for(int dx = -dim; dx <= dim; ++dx) { //fuck bounding box, let's hardcode it
for(int dy = -dim; dy <= dim; ++dy) {
for(auto &tile : init) {
std::vector<Point> vert;
for(auto &p : tile.coords) {
float ax = dx * offsets[0] + dy * offsets[2];
float ay = dx * offsets[1] + dy * offsets[3];
vert.push_back(Point(p.x + ax, p.y + ay));
}
tiles.push_back(Tile(tiles.size(), vert));
tiles.back().color = tile.color;
tiles.back().state = tile.state;
}
}
}
//stupid bruteforce solution, but who's got time to think..
for(Tile &tile : tiles) { //find neighbours for each cell
for(Tile &t : tiles) {
if(tile == t) continue;
for(Point &p : t.coords) {
for(Point &pt : tile.coords) {
if(glm::distance(p, pt) < 0.01 ) {
tile.neighbours.insert(&t);
break;
}
}
}
}
assert(tile.neighbours.size() <= 9);
}
}
void init() {
for(auto &t : tiles) {
if(rand() % 100 < CHANCE_TO_LIVE) {
t.state = Tile::BORN;
}
else {
t.state = Tile::DEAD;
}
}
}
void update() {
for(auto &tile: tiles) {
//check colors
switch(tile.state) {
case Tile::BORN: //animate birth
tile.color.g -= 0.05;
tile.color.b += 0.05;
if(tile.color.b > 0.9) {
tile.state = Tile::ALIVE;
}
break;
case Tile::DIED: //animate death
tile.color += 0.05;
if(tile.color.g > 0.9) {
tile.state = Tile::DEAD;
}
break;
}
//fix colors after animation
switch(tile.state) {
case Tile::ALIVE:
tile.color = Color(0, 0, 1);
break;
case Tile::DEAD:
tile.color = Color(1, 1, 1);
break;
}
//draw polygons
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glBegin(GL_POLYGON);
glColor3f(tile.color.r, tile.color.g, tile.color.b);
for(auto &pt : tile.coords) {
glVertex2f(pt.x, pt.y); //haha so oldschool!
}
glEnd();
}
//draw grid
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glColor3f(0, 0, 0);
for(auto &tile : tiles) {
glBegin(GL_POLYGON);
Point c; //centroid of tile
for(auto &pt : tile.coords) {
glVertex2f(pt.x, pt.y);
c += pt;
}
glEnd();
if(debug) {
c /= (float) Tile::VERTICES;
glRasterPos2f(c.x - 0.025, c.y - 0.01);
font.Render(std::to_string(tile.ID).c_str()); //
}
}
if(!run) {
return;
}
//compute new generation
for(Tile &tile: tiles) {
tile.nextState = tile.state; //initialize next state
int c = 0;
for(auto *n : tile.neighbours) {
if(n->isAlive()) c++;
}
switch(c) {
case 2:
break;
case 3:
if(tile.isDead()) {
tile.nextState = animate ? Tile::BORN : Tile::ALIVE;
tile.color = Color(0, 1, 0);
}
break;
default:
if(tile.isAlive()) {
tile.nextState = animate ? Tile::DIED : Tile::DEAD;
tile.color = Color(1, 0, 0);
}
break;
}
}
//switch state to new
for(Tile &tile: tiles) {
tile.updateState();
}
}
void stop() {run = false;}
void switchRun() {run = !run;}
bool isRun() {return run;}
void switchAnim() {animate = !animate;}
bool isAnim() {return animate;}
void switchExportGif() {exportGif = !exportGif;}
bool isExportGif() {return exportGif;}
void switchDebug() {debug = !debug;}
bool isDebug() const {return debug;}
private:
static float getAngle(const Point &p0, const Point &p1, Point const &p2) {
return atan2(p2.y - p0.y, p2.x - p0.x) - atan2(p1.y - p0.y, p1.x - p0.x);
}
static float getAngle(const Point &p0, const Point &p1) {
return atan2(p1.y - p0.y, p1.x - p0.x);
}
};
class Controlls {
Game *game;
std::vector<Magick::Image> *gif;
Controlls() : game(nullptr), gif(nullptr) {}
public:
static Controlls& getInstance() {
static Controlls instance;
return instance;
}
static void keyboardAction(GLFWwindow* window, int key, int scancode, int action, int mods) {
getInstance().keyboardActionImpl(key, action);
}
void setGame(Game *game) {
this->game = game;
}
void setGif(std::vector<Magick::Image> *gif) {
this->gif = gif;
}
private:
void keyboardActionImpl(int key, int action) {
if(!game || action == GLFW_RELEASE) {
return;
}
switch (key) {
case 'R':
game->stop();
game->init();
if(gif) gif->clear();
break;
case GLFW_KEY_SPACE:
game->switchRun();
break;
case 'A':
game->switchAnim();
break;
case 'D':
game->switchDebug();
break;
break;
case 'G':
game->switchExportGif();
break;
};
}
};
int main(int argc, char** argv) {
const int width = 620; //window size
const int height = 620;
const std::string window_title ("Game of life!");
const std::string font_file ("/usr/share/fonts/truetype/arial.ttf");
const std::string gif_file ("./gol.gif");
if(!glfwInit()) return 1;
GLFWwindow* window = glfwCreateWindow(width, height, window_title.c_str(), NULL, NULL);
glfwSetWindowPos(window, 100, 100);
glfwMakeContextCurrent(window);
GLuint err = glewInit();
if (err != GLEW_OK) return 2;
FTGLPixmapFont font(font_file.c_str());
if(font.Error()) return 3;
font.FaceSize(8);
std::vector<Magick::Image> gif; //gif export
std::vector<GLfloat> pixels(3 * width * height);
Game gol(font);
gol.init();
Controlls &controlls = Controlls::getInstance();
controlls.setGame(&gol);
controlls.setGif(&gif);
glfwSetKeyCallback(window, Controlls::keyboardAction);
glClearColor(1.f, 1.f, 1.f, 0);
while(!glfwWindowShouldClose(window) && !glfwGetKey(window, GLFW_KEY_ESCAPE)) {
glClear(GL_COLOR_BUFFER_BIT);
gol.update();
//add layer to gif
if(gol.isExportGif()) {
glReadPixels(0, 0, width, height, GL_RGB, GL_FLOAT, &pixels[0]);
Magick::Image image(width, height, "RGB", Magick::FloatPixel, &pixels[0]);
image.animationDelay(50);
gif.push_back(image);
}
std::string info = "ANIMATE (A): ";
info += gol.isAnim() ? "ON " : "OFF";
info += " | DEBUG (D): ";
info += gol.isDebug() ? "ON " : "OFF";
info += " | EXPORT GIF (G): ";
info += gol.isExportGif() ? "ON " : "OFF";
info += gol.isRun() ? " | STOP (SPACE)" : " | START (SPACE)";
font.FaceSize(10);
glRasterPos2f(-.95f, -.99f);
font.Render(info.c_str());
if(gol.isDebug()) font.FaceSize(8);
if(!gol.isDebug()) usleep(50000); //not so fast please!
glfwSwapBuffers(window);
glfwPollEvents();
}
//save gif to file
if(gol.isExportGif()) {
std::cout << "saving " << gif.size() << " frames to gol.gif\n";
gif.back().write("./last.png");
Magick::writeImages(gif.begin(), gif.end(), gif_file);
}
glfwTerminate();
return 0;
}