#include // printf, snprintf #include // uint16_t #include // srand, rand #include // time #include // cos, sin #include "glad.h" #include "glfw3.h" /* you can compile this (windows only, debug version, assuming you have cl.exe available) using: cl.exe glad.c main.cpp /nologo /W3 /MTd /Od /Zi /RTC1 /fsanitize=address /link /out:game.exe /incremental:no glfw3.lib the .exe will be around 1.5mb in this case. or, in release version with a smaller .exe size and better perf (181kb with clang-cl, dang just barely missed 128kb total size!), no debugging symbols etc. clang-cl.exe glad.c main.cpp /nologo /W3 /O2 /GS /clang:-fno-asynchronous-unwind-tables /link /out:game.exe /fixed /incremental:no /opt:icf /opt:ref libvcruntime.lib glfw3.lib OR cl.exe glad.c main.cpp /nologo /W3 /O2 /GS /link /out:game.exe /fixed /incremental:no /opt:icf /opt:ref libvcruntime.lib glfw3.lib the first 10-20 frames in debug mode are prone to run below 60fps on my machine, which I unfortunately wasn't able to fix in 16 hours - my computer is fairly weak though, it's about 10 years old and has no external GPU. You may fare better :). Subsequent frames perfome many times better. - [Y] - snake-like 0 player game - [Y] - need to support on the order of 8k+ snakes concurrently (we're going to choose a max of 8192) - [Y] - if the head of a snake touches the tail of another, the snake who's head touched the tail will merge up with the 'eaten' snake. - [Y] - all snakes start with one segment - [Y] - when there is only one snake left, the game must restart - [Y?] - game data under 128kb - achieved if this means game state, not quite if it means .exe size (we're about ~1mb .exe size after trying to minimize size) - [Y] - 60fps minimum, or locked - [Y] - static memory allocation, avoid heap - [Y] - minimal or zero usage of libraries (except glfw and opengl) */ static GLFWwindow *Window; inline bool checkError__(int line) { GLenum errorCode; while ((errorCode = glGetError()) != GL_NO_ERROR) { switch (errorCode) { case GL_INVALID_ENUM: printf("%d, INVALID_ENUM", line); break; case GL_INVALID_VALUE: printf("%d, INVALID_VALUE", line); break; case GL_INVALID_OPERATION: printf("%d, INVALID_OPERATION", line); break; case GL_OUT_OF_MEMORY: printf("%d, OUT_OF_MEMORY", line); break; case GL_INVALID_FRAMEBUFFER_OPERATION: printf("%d, INVALID_FRAMEBUFFER_OPERATION", line); break; } return true; } return false; } #define checkError() checkError__(__LINE__) static const float PI = 3.14159f; //-------------------------------------------------------------------------------- // game-data // between QUADRANTS and pointBuffer, the only real data structures in use, // you have 116.736kb of game state, assuming 8192 snakes. int windowWidth; int windowHeight; struct Snake { // coordinates are normalized unsigned integers. // |numSegments| is just a count, where 0 indicates a dead snake. uint16_t numSegments, headX, headY, theta; }; static constexpr float SNAKE_SEGMENT_RADIUS = 8; // glPointSize static constexpr unsigned int NUM_SNAKES = 8192; // must be greater than 1 struct Rectangle { uint16_t left, bottom, right, top; }; static constexpr uint32_t NUM_QUADRANTS = 64; static constexpr unsigned int SNAKES_PER_QUADRANT = NUM_SNAKES/NUM_QUADRANTS; struct Quadrant { Snake snakes[SNAKES_PER_QUADRANT]; Rectangle rect; }; static Quadrant QUADRANTS[NUM_QUADRANTS]; struct Point { uint16_t x, y; uint16_t weight; }; static Point pointBuffer[NUM_SNAKES]; // each snake starts as one segment which is represented as a point, so you can have at most NUM_SNAKES points. uint32_t numDead = 0; uint32_t longest = 0; uint32_t numPoints = 0; double msPerFrame = 0.0; double deltaTime = 0.0; double lastFrameCountTime = 0.0; double lastFrameStartTime = 0.0; bool doFancyRestart = false; long long int numFramesInLastSecond = 0; long long int frameCounter = 0; //-------------------------------------------------------------------------------- static inline uint16_t normalizeUint(uint16_t input, uint32_t oldMax) { return input * USHRT_MAX / oldMax; } static inline float invlerp(float a, float b, float v) { return (v - a) / (b - a); } static inline float lerp(float a, float b, float t) { return a + t * (b - a); } static inline float snakeThetaToRadians(uint16_t a) { return ((float)a/(float)USHRT_MAX)*2.f*PI; } static inline uint16_t radiansToSnakeTheta(float r) { return (uint16_t) ((r + (0.f*PI)) / (2.f*PI) * (float)USHRT_MAX); } static inline void splitRectangleIntoQuadrants(Rectangle input, Rectangle quadrants[4]) { auto centerX = (input.left + input.right) / 2; auto centerY = (input.top + input.bottom) / 2; // top-left quadrant, proceeding clockwise quadrants[0].left = input.left; quadrants[0].right = centerX; quadrants[0].top = input.top; quadrants[0].bottom = centerY; quadrants[1].left = centerX; quadrants[1].right = input.right; quadrants[1].top = input.top; quadrants[1].bottom = centerY; quadrants[2].left = centerX; quadrants[2].right = input.right; quadrants[2].top = centerY; quadrants[2].bottom = input.bottom; quadrants[3].left = input.left; quadrants[3].right = centerX; quadrants[3].top = centerY; quadrants[3].bottom = input.bottom; } static inline uint16_t randInRange(unsigned int min, unsigned int max) { const unsigned int range = max - min + 1; return (uint16_t) (min + (unsigned int)((double)rand() / (RAND_MAX + 1.0) * range)); } static inline void calculateSegment(Snake* snake, uint16_t segmentNumber, Point* outPoint) { const float weight = (float)segmentNumber / (float)snake->numSegments; const float radians = snakeThetaToRadians(snake->theta); const float dx = cos(radians); const float dy = sin(radians); outPoint->x = snake->headX + dx * segmentNumber * SNAKE_SEGMENT_RADIUS; outPoint->y = snake->headY + dy * segmentNumber * SNAKE_SEGMENT_RADIUS; outPoint->weight = (uint16_t) (weight * USHRT_MAX); } static inline void initializeQuadrants(int windowWidth, int windowHeight) { Rectangle windowQuad = { 0, 0, normalizeUint(windowWidth, windowWidth), normalizeUint(windowHeight, windowHeight) }; Rectangle topLevelQuads[4]; splitRectangleIntoQuadrants(windowQuad, topLevelQuads); // there's probably a better way to iteratively construct a quadtree... for (int i = 0; i < 4; i++) { Rectangle iquads[4]; splitRectangleIntoQuadrants(topLevelQuads[i], iquads); for (int j = 0; j < 4; j++) { Rectangle jquads[4]; splitRectangleIntoQuadrants(iquads[j], jquads); for (int k = 0; k < 4; k++) { QUADRANTS[i*16+j*4+k].rect = jquads[k]; } } } for (int i = 0; i < NUM_QUADRANTS; i++) { Quadrant* q= &QUADRANTS[i]; for (int j = 0; j < SNAKES_PER_QUADRANT; j++) { Snake* snake = &q->snakes[j]; snake->numSegments = 1; snake->headX = randInRange(q->rect.left, q->rect.right); snake->headY = randInRange(q->rect.bottom, q->rect.top); //printf("Snake #%d, quadrant %d - head (x/y): %u/%u, numSegments: %u\n", j+1, i, snake->headX, snake->headY, snake->numSegments); } } } static inline unsigned int computeQuadrant(int x, int y, int left, int top, int bottom, int right) { if (x < left || y < bottom || x > right || y > top) { printf("outside rect!\n"); return -1; } else if (x < right/2 && y < top/2) { return 0; } else if (x >= right/2 && y < top/2) { return 1; } else if (x < right/2 && y >= top/2) { return 2; } else { return 3; } } static inline bool findNearestSnakeInQuad( Quadrant* quad, Snake* snake, int searchingSnakeIndex, float& nearestD, uint16_t& nearestX, uint16_t& nearestY, bool* outHadLiveSnake ) { for (int j = 0; j < SNAKES_PER_QUADRANT; j++) { if (searchingSnakeIndex == j) continue; Snake* other = &quad->snakes[j]; if (other->numSegments == 0) continue; // dead snake, can't bite it // if we have at least one snake that has a segment, that means this quad has at least one other live snake. if (outHadLiveSnake) *outHadLiveSnake = true; // find tail coordinates for this snake Point tail; calculateSegment(other, other->numSegments-1, &tail); // compute distance, check if it's closer than previously recorded float dx = ((float)snake->headX - (float)tail.x); float dy = ((float)snake->headY - (float)tail.y); float distance = sqrt(dx*dx + dy*dy); if (distance < nearestD) { nearestD = distance; nearestX = tail.x; nearestY = tail.y; if (distance < SNAKE_SEGMENT_RADIUS*3) { // if we're close enough, we can call it a bite and early-out. snake->numSegments += other->numSegments; other->numSegments = 0; return 1; } } } return 0; } static inline bool updateQuadrant(unsigned int quadIndex) { Quadrant* quad = &QUADRANTS[quadIndex]; for (int i = 0; i < SNAKES_PER_QUADRANT; i++) { // update each snake in the quad. there are only really a few things to do: // - find nearest neighbour to snake // - check if bite // - move and rotate snake bool foundAtLeastOneOtherLiveSnakeInQuad = false; Snake* snake = &quad->snakes[i]; uint16_t numSegments = snake->numSegments; if (numSegments == 0) continue; if (numSegments > longest) { longest = numSegments; // just for fun. } // we now linear search for nearest tail to bite, early-outing on dead snakes, // and very-close snakes (we technically do not always garuntee finding the 'nearest') const uint16_t headX = snake->headX; const uint16_t headY = snake->headY; float nearestD = 999999.f; // arbitrary large-ish number uint16_t nearestX = 0; uint16_t nearestY = 0; if (findNearestSnakeInQuad(quad, snake, i, nearestD, nearestX, nearestY, &foundAtLeastOneOtherLiveSnakeInQuad)) { numDead++; goto nextSnake; // we killed another snake, we can move on to the next snake. }; if (!foundAtLeastOneOtherLiveSnakeInQuad) { // we found 0 snakes in this quad (besides ourselves) // this circumstance can only arise when there are a small number of snakes left // , less than 2k at least, so we can just lazily iterate the rest of the quadrants. for (int j = 0; j < NUM_QUADRANTS; j++) { if (quadIndex == j) continue; if (findNearestSnakeInQuad(&QUADRANTS[j], snake, -1, nearestD, nearestX, nearestY, &foundAtLeastOneOtherLiveSnakeInQuad)) { numDead++; goto nextSnake; // we killed another snake, we can move on to the next snake, after appending our draw data } } } if (!foundAtLeastOneOtherLiveSnakeInQuad) { // we must be the only snake left! return true; } // do the movement and rotation for this snake. const float angle = atan2f((float)headY - (float)nearestY, (float)headX - (float)nearestX); // it probably makes more sense for the smaller guys to be faster, but the simulation gets quite slow towards the end of it, so I do the opposite. const float dx = -cos(angle) * 500.f*log(0.25f*snake->numSegments + 1) * deltaTime; const float dy = -sin(angle) * 500.f*log(0.25f*snake->numSegments + 1) * deltaTime; snake->headX += dx; snake->headY += dy; #if 1 // move the snake's angle towards angle |angle|, with some fixed radial speed, smoothly const float snakeAngle = snakeThetaToRadians(snake->theta); const float angleDelta = angle - snakeAngle; //snake->theta = radiansToSnakeTheta(snakeAngle + ((2.f*PI*angleDelta)*0.0015f/(2.f*PI)); //snake->theta = radiansToSnakeTheta(fmin(snakeAngle + (2.f*PI*angleDelta)*(deltaTime/snake->numSegments)/(2.f*PI), angle)); snake->theta = radiansToSnakeTheta(snakeAngle + (angleDelta+2.f*PI)*deltaTime*100.f/snake->numSegments); #else snake->theta = radiansToSnakeTheta(angle); #endif // at this point, we know everything we need to know about this snake to populate the draw data for it. // we have the head position, an angle, and a count of the number of segments, so we use this to generate // points from head --> tail. for (uint16_t p = 0; p < snake->numSegments; p++) { Point* point = &pointBuffer[numPoints++]; calculateSegment(snake, p, point); } nextSnake: continue; } return false; } int main(void) { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_VISIBLE, false); // hide window initially - while we set up stuff. show after. glfwWindowHint(GLFW_RESIZABLE, false); // don't allow the user to re-size the window glfwWindowHint(GLFW_CENTER_CURSOR, true); // center the cursor in the window when opening a fullscreen window glfwWindowHint(GLFW_FOCUS_ON_SHOW, true); // focus the window when glfwShowWindow is called #ifdef __APPLE__ // removes all deprecated opengl functionality from older opengl versions, required on MacOSX glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif GLFWmonitor* primaryMonitor = glfwGetPrimaryMonitor(); const GLFWvidmode* primaryMonitorMode = glfwGetVideoMode(primaryMonitor); float initialWidth = 1138.0f; float initialHeight = 640.0f; // scale up the dimensions of the initial size of the window until it exceeds the size of the screen, then go with the one just before that while (1) { if ((initialHeight * 1.2f > primaryMonitorMode->height) || (initialWidth * 1.2f) > primaryMonitorMode->width) { break; } initialHeight *= 1.2f; initialWidth *= 1.2f; } windowWidth = (int) initialWidth; windowHeight = (int) initialHeight; Window = glfwCreateWindow(windowWidth, windowHeight, "flOw MMo", nullptr, nullptr); if (Window == nullptr) { printf("%s", "Failed to initialize GLFWwindow\n"); return 1; } glfwMakeContextCurrent(Window); //-------------------------------------------------------------------------------- // initialize glad, resolve OpenGL function pointers if (!gladLoadGLLoader((GLADloadproc) glfwGetProcAddress)) { printf("%s", "Failed to initialize GLAD\n"); return 1; } //-------------------------------------------------------------------------------- // compile and link our shader. // my apologies if this fails compilation on your machine. // My main computer's OpenGL driver has been known to allow compilations where others fail it. int success; char infoLog[512]; // vertex... const char* vertexShaderCode = R"( #version 330 core layout (location = 0) in vec2 aPos; layout (location = 1) in float aWeight; flat out float vWeight; void main() { vWeight = aWeight; gl_Position = vec4(aPos*2.0-1.0, 0.0, 1.0); gl_PointSize = 10 + (1.0f - aWeight)*10; } )"; unsigned int vertexShaderId = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShaderId, 1, &vertexShaderCode, nullptr); glCompileShader(vertexShaderId); glGetShaderiv(vertexShaderId, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShaderId, 512, nullptr, infoLog); printf("Failure compiling vertex shader!\n%s\nSource: %s\n", infoLog, vertexShaderCode); return 1; } // fragment... const char* fragmentShaderCode = R"( #version 330 core flat in float vWeight; out vec4 FragColor; void main() { FragColor = vec4(vWeight + 0.5, 0.5f * vWeight, 0.2f / (vWeight+0.01), 1.0f); } )"; unsigned int fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShaderId, 1, &fragmentShaderCode, nullptr); glCompileShader(fragmentShaderId); glGetShaderiv(fragmentShaderId, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShaderId, 512, nullptr, infoLog); printf("Failure compiling fragment shader!\n%s\nSource: %s\n", infoLog, fragmentShaderCode); return 1; } unsigned int shaderId = glCreateProgram(); glAttachShader(shaderId, vertexShaderId); glAttachShader(shaderId, fragmentShaderId); glLinkProgram(shaderId); glGetProgramiv(shaderId, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderId, 512, nullptr, infoLog); printf("Failure linking shader!\n%s\n", infoLog); return 1; } checkError(); // use it, we only have one shader glUseProgram(shaderId); checkError(); //-------------------------------------------------------------------------------- srand(time(nullptr)); for (int i = 0; i < rand(); i++) rand(); // discard first few calls, randomly. 'rand()' has poor entropy // initialize the game state. set up all the initial snake positions, etc. initializeQuadrants(windowWidth, windowHeight); //-------------------------------------------------------------------------------- // setup the VAO/VBO unsigned int VAO, VBO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_UNSIGNED_SHORT, GL_TRUE, sizeof(Point), (void*)offsetof(Point, x)); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 1, GL_UNSIGNED_SHORT, GL_TRUE, sizeof(Point), (void*)offsetof(Point, weight)); //-------------------------------------------------------------------------------- // set global opengl state glClearColor(0.25f, 0.52f, 0.54f, 1.0f); glEnable(GL_PROGRAM_POINT_SIZE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_CULL_FACE); glfwShowWindow(Window); // make window visible before entering main loop glfwRequestWindowAttention(Window); while (!glfwWindowShouldClose(Window)) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // calculate timing metrics, delta time, etc. double currentTime = glfwGetTime(); frameCounter++; if (currentTime - lastFrameCountTime >= 1.0) { msPerFrame = 1000.0/frameCounter; numFramesInLastSecond = frameCounter; frameCounter = 0; lastFrameCountTime += 1.0; static char windowTitle[256]; snprintf(windowTitle, 256, "flOw MMo - %.2f ms per frame, %lld fps, delta %.2f, num snakes: %u, num alive: %u, dead: %u, longest boi: %u", msPerFrame, numFramesInLastSecond, deltaTime, NUM_SNAKES, NUM_SNAKES-numDead, numDead, longest); glfwSetWindowTitle(Window, windowTitle); // <-- this call is very slow :( } deltaTime = currentTime - lastFrameStartTime; lastFrameStartTime = currentTime; // set ourselves to close if you press escape if (glfwGetKey(Window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(Window, true); continue; } static double lastUpdateTime = 0.0f; if ((currentTime - lastUpdateTime) < 0.0167f) { continue; // we already updated in the last 16ms, skip to next frame } if (doFancyRestart) { initializeQuadrants(windowWidth, windowHeight); doFancyRestart = false; } else { // update game state: bool gameOver = false; for (int q = 0; q < NUM_QUADRANTS; q++) { gameOver = updateQuadrant(q); if (gameOver) break; } if (gameOver) { printf("game over!\n"); doFancyRestart = true; continue; } } // upload draw data and render glBufferData(GL_ARRAY_BUFFER, sizeof(Point)*NUM_SNAKES, nullptr, GL_DYNAMIC_DRAW); glBufferData(GL_ARRAY_BUFFER, sizeof(Point)*NUM_SNAKES, pointBuffer, GL_DYNAMIC_DRAW); glDrawArrays(GL_POINTS, 0, numPoints); numPoints = 0; checkError(); glfwSwapBuffers(Window); glfwPollEvents(); // performs better on windows at the end of frame vs. start } glDeleteShader(vertexShaderId); glDeleteShader(fragmentShaderId); glDeleteProgram(shaderId); glfwTerminate(); return 0; }