First working prototype

This commit is contained in:
2026-03-16 22:32:20 -03:00
commit 356dfaf3a8
4 changed files with 534 additions and 0 deletions

453
main.cpp Normal file
View File

@@ -0,0 +1,453 @@
#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <SDL2/SDL_opengl.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <vector>
#include <array>
#include <cmath>
#include <cstring>
#include <unordered_map>
// Instalation
// On root:
// mkdir build
// cmake .
// make -j$(nproc)
// ./minicraft
// ─── Constants ───────────────────────────────────────────────────────────────
static const int SCREEN_W = 1280;
static const int SCREEN_H = 720;
static const int CHUNK_SIZE = 16;
static const int WORLD_H = 16;
static const float MOVE_SPEED = 5.0f;
static const float MOUSE_SENS = 0.12f;
static const float GRAVITY = -20.0f;
static const float JUMP_VEL = 8.0f;
// ─── Block types ─────────────────────────────────────────────────────────────
enum BlockType : uint8_t { AIR=0, GRASS, DIRT, STONE, WOOD, LEAVES, SAND, WATER };
struct BlockColor { float r,g,b; };
static const BlockColor BLOCK_COLORS[] = {
{0,0,0}, // AIR
{0.40f,0.72f,0.24f}, // GRASS
{0.55f,0.40f,0.22f}, // DIRT
{0.55f,0.55f,0.55f}, // STONE
{0.45f,0.30f,0.15f}, // WOOD
{0.20f,0.60f,0.15f}, // LEAVES
{0.90f,0.85f,0.55f}, // SAND
{0.20f,0.45f,0.90f}, // WATER
};
// face shade multipliers (top, bottom, front, back, left, right)
static const float FACE_SHADE[] = {1.0f, 0.5f, 0.8f, 0.8f, 0.65f, 0.65f};
// ─── World ───────────────────────────────────────────────────────────────────
static uint8_t WORLD[CHUNK_SIZE][WORLD_H][CHUNK_SIZE];
static int terrainHeight(int x, int z) {
float h = 5.0f
+ 3.0f * sinf(x * 0.25f) * cosf(z * 0.20f)
+ 2.0f * sinf(x * 0.10f + z * 0.13f)
+ 1.0f * cosf(x * 0.40f - z * 0.35f);
return (int)h;
}
static void generateWorld() {
memset(WORLD, AIR, sizeof(WORLD));
for (int x = 0; x < CHUNK_SIZE; ++x)
for (int z = 0; z < CHUNK_SIZE; ++z) {
int top = terrainHeight(x, z);
if (top < 1) top = 1;
if (top >= WORLD_H) top = WORLD_H - 1;
for (int y = 0; y < WORLD_H; ++y) {
if (y == 0) WORLD[x][y][z] = STONE;
else if (y < top - 3) WORLD[x][y][z] = STONE;
else if (y < top) WORLD[x][y][z] = DIRT;
else if (y == top) WORLD[x][y][z] = (top <= 3) ? SAND : GRASS;
}
// Small trees
if (top > 3 && top < WORLD_H - 5 && (x + z * 7) % 13 == 0) {
int trunk = top + 1;
for (int t = trunk; t < trunk + 3 && t < WORLD_H; ++t)
WORLD[x][t][z] = WOOD;
for (int dx = -1; dx <= 1; ++dx)
for (int dz = -1; dz <= 1; ++dz)
for (int dy = trunk + 2; dy <= trunk + 4; ++dy) {
int nx = x+dx, nz = z+dz;
if (nx>=0&&nx<CHUNK_SIZE&&nz>=0&&nz<CHUNK_SIZE&&dy<WORLD_H)
if (WORLD[nx][dy][nz] == AIR)
WORLD[nx][dy][nz] = LEAVES;
}
}
}
}
// ─── Shaders ─────────────────────────────────────────────────────────────────
static const char* VS_SRC = R"(
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
out vec3 vColor;
uniform mat4 uMVP;
void main(){
gl_Position = uMVP * vec4(aPos, 1.0);
vColor = aColor;
}
)";
static const char* FS_SRC = R"(
#version 330 core
in vec3 vColor;
out vec4 fragColor;
void main(){
fragColor = vec4(vColor, 1.0);
}
)";
static GLuint compileShader(GLenum type, const char* src) {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buf[512]; glGetShaderInfoLog(s, 512, nullptr, buf);
std::cerr << "Shader error: " << buf << "\n";
}
return s;
}
static GLuint buildProgram() {
GLuint vs = compileShader(GL_VERTEX_SHADER, VS_SRC);
GLuint fs = compileShader(GL_FRAGMENT_SHADER, FS_SRC);
GLuint p = glCreateProgram();
glAttachShader(p, vs); glAttachShader(p, fs);
glLinkProgram(p);
glDeleteShader(vs); glDeleteShader(fs);
return p;
}
// ─── Mesh builder ────────────────────────────────────────────────────────────
struct Vertex { float x,y,z,r,g,b; };
// Checks if we should draw a face against a neighboring block
static bool shouldDrawFace(uint8_t currentBlock, int nx, int ny, int nz) {
if (nx<0 || nx>=CHUNK_SIZE || ny<0 || ny>=WORLD_H || nz<0 || nz>=CHUNK_SIZE) return true;
uint8_t nb = WORLD[nx][ny][nz];
if (nb == AIR) return true;
// Transparent blocks like water/leaves hide internal faces of the *same* type,
// but allow drawing faces of different adjacent blocks.
if (nb == WATER || nb == LEAVES) {
return currentBlock != nb;
}
return false; // Solid opaque block, hide the face
}
static void addFace(std::vector<Vertex>& verts, std::vector<GLuint>& idx,
glm::vec3 v0, glm::vec3 v1, glm::vec3 v2, glm::vec3 v3,
float r, float g, float b, float shade) {
GLuint base = (GLuint)verts.size();
float sr=r*shade, sg=g*shade, sb=b*shade;
verts.push_back({v0.x,v0.y,v0.z,sr,sg,sb});
verts.push_back({v1.x,v1.y,v1.z,sr,sg,sb});
verts.push_back({v2.x,v2.y,v2.z,sr,sg,sb});
verts.push_back({v3.x,v3.y,v3.z,sr,sg,sb});
idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3});
}
static void buildMesh(std::vector<Vertex>& verts, std::vector<GLuint>& idx) {
verts.clear(); idx.clear();
for (int x=0;x<CHUNK_SIZE;++x)
for (int y=0;y<WORLD_H;++y)
for (int z=0;z<CHUNK_SIZE;++z) {
uint8_t b = WORLD[x][y][z];
if (b == AIR) continue;
float r=BLOCK_COLORS[b].r, g=BLOCK_COLORS[b].g, bl=BLOCK_COLORS[b].b;
float bx=x,by=y,bz=z;
// FIXED Winding Orders:
// Top (+Y)
if (shouldDrawFace(b, x,y+1,z))
addFace(verts,idx,{bx,by+1,bz+1},{bx+1,by+1,bz+1},{bx+1,by+1,bz},{bx,by+1,bz},r,g,bl,FACE_SHADE[0]);
// Bottom (-Y)
if (shouldDrawFace(b, x,y-1,z))
addFace(verts,idx,{bx,by,bz},{bx+1,by,bz},{bx+1,by,bz+1},{bx,by,bz+1},r,g,bl,FACE_SHADE[1]);
// Front (+Z)
if (shouldDrawFace(b, x,y,z+1))
addFace(verts,idx,{bx,by,bz+1},{bx+1,by,bz+1},{bx+1,by+1,bz+1},{bx,by+1,bz+1},r,g,bl,FACE_SHADE[2]);
// Back (-Z)
if (shouldDrawFace(b, x,y,z-1))
addFace(verts,idx,{bx+1,by,bz},{bx,by,bz},{bx,by+1,bz},{bx+1,by+1,bz},r,g,bl,FACE_SHADE[3]);
// Left (-X)
if (shouldDrawFace(b, x-1,y,z))
addFace(verts,idx,{bx,by,bz},{bx,by,bz+1},{bx,by+1,bz+1},{bx,by+1,bz},r,g,bl,FACE_SHADE[4]);
// Right (+X)
if (shouldDrawFace(b, x+1,y,z))
addFace(verts,idx,{bx+1,by,bz+1},{bx+1,by,bz},{bx+1,by+1,bz},{bx+1,by+1,bz+1},r,g,bl,FACE_SHADE[5]);
}
}
// ─── Camera / Player ─────────────────────────────────────────────────────────
struct Camera {
glm::vec3 pos{8,12,8};
float yaw=-135.0f, pitch=0.0f;
glm::vec3 vel{0,0,0};
bool onGround=false;
glm::vec3 forward() const {
float yRad=glm::radians(yaw), pRad=glm::radians(pitch);
return glm::normalize(glm::vec3(cosf(pRad)*cosf(yRad),sinf(pRad),cosf(pRad)*sinf(yRad)));
}
glm::vec3 right() const { return glm::normalize(glm::cross(forward(),{0,1,0})); }
glm::mat4 view() const { return glm::lookAt(pos, pos+forward(), {0,1,0}); }
};
// Player AABB: half-width HW on X/Z, height PH, eyes at top.
static const float HW = 0.3f; // half-width
static const float PH = 1.8f; // player height (feet to eye)
static bool solidAt(float fx, float fy, float fz) {
int x=(int)floorf(fx), y=(int)floorf(fy), z=(int)floorf(fz);
if (y < 0) return true; // solid floor under world
if (x < 0 || x >= CHUNK_SIZE || z < 0 || z >= CHUNK_SIZE) return true; // invisible walls
if (y >= WORLD_H) return false;
uint8_t b = WORLD[x][y][z];
return b != AIR && b != WATER;
}
// FIXED: Now correctly checks all blocks intersecting the player's bounding volume
static bool aabbSolid(float px, float py, float pz) {
int minX = (int)floorf(px - HW);
int maxX = (int)floorf(px + HW);
int minY = (int)floorf(py - PH);
int maxY = (int)floorf(py + 0.1f);
int minZ = (int)floorf(pz - HW);
int maxZ = (int)floorf(pz + HW);
for (int x = minX; x <= maxX; ++x) {
for (int y = minY; y <= maxY; ++y) {
for (int z = minZ; z <= maxZ; ++z) {
if (solidAt((float)x, (float)y, (float)z)) return true;
}
}
}
return false;
}
static void moveCamera(Camera& cam, const glm::vec3& move, float dt) {
// ── X axis ──
cam.pos.x += move.x * dt;
if (aabbSolid(cam.pos.x, cam.pos.y, cam.pos.z)) {
cam.pos.x -= move.x * dt; // Undo move if collided
}
// ── Z axis ──
cam.pos.z += move.z * dt;
if (aabbSolid(cam.pos.x, cam.pos.y, cam.pos.z)) {
cam.pos.z -= move.z * dt; // Undo move if collided
}
// ── Y axis (gravity + jump) ──
cam.vel.y += GRAVITY * dt;
cam.pos.y += cam.vel.y * dt;
if (cam.vel.y < 0) {
// Moving downward
if (aabbSolid(cam.pos.x, cam.pos.y, cam.pos.z)) {
// Snap feet to top of the block below
cam.pos.y = floorf(cam.pos.y - PH) + 1.0f + PH + 0.001f;
cam.vel.y = 0.0f;
cam.onGround = true;
} else {
cam.onGround = false;
}
} else {
// Moving upward
if (aabbSolid(cam.pos.x, cam.pos.y, cam.pos.z)) {
// Hit ceiling — snap head downwards
cam.pos.y = floorf(cam.pos.y + 0.1f) - 0.1f - 0.001f;
cam.vel.y = 0.0f;
}
cam.onGround = false;
}
}
// ─── Ray-cast for block placement/removal ────────────────────────────────────
static bool raycast(const Camera& cam, glm::ivec3& hit, glm::ivec3& prev) {
glm::vec3 dir = cam.forward();
glm::vec3 p = cam.pos;
glm::ivec3 last{(int)floorf(p.x), (int)floorf(p.y), (int)floorf(p.z)};
for (float t=0; t<8.0f; t+=0.05f) {
glm::vec3 rp = p + dir*t;
glm::ivec3 bp{(int)floorf(rp.x),(int)floorf(rp.y),(int)floorf(rp.z)};
// Skip out of bounds, but don't break the ray
if (bp.x<0||bp.x>=CHUNK_SIZE||bp.y<0||bp.y>=WORLD_H||bp.z<0||bp.z>=CHUNK_SIZE) {
last = bp;
continue;
}
if (WORLD[bp.x][bp.y][bp.z] != AIR && WORLD[bp.x][bp.y][bp.z] != WATER) {
hit = bp; prev = last; return true;
}
last = bp;
}
return false;
}
// ─── Main ────────────────────────────────────────────────────────────────────
int main() {
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_Window* win = SDL_CreateWindow("MiniCraft",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
SCREEN_W, SCREEN_H,
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
SDL_GLContext ctx = SDL_GL_CreateContext(win);
SDL_GL_SetSwapInterval(1);
glewExperimental = GL_TRUE;
glewInit();
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glClearColor(0.53f, 0.81f, 0.98f, 1.0f);
GLuint prog = buildProgram();
GLuint VAO, VBO, EBO;
glGenVertexArrays(1,&VAO);
glGenBuffers(1,&VBO);
glGenBuffers(1,&EBO);
generateWorld();
std::vector<Vertex> verts;
std::vector<GLuint> idx;
buildMesh(verts, idx);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, verts.size()*sizeof(Vertex), verts.data(), GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size()*sizeof(GLuint), idx.data(), GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
glm::mat4 proj = glm::perspective(glm::radians(70.0f),(float)SCREEN_W/SCREEN_H,0.05f,300.0f);
GLint mvpLoc = glGetUniformLocation(prog,"uMVP");
SDL_SetRelativeMouseMode(SDL_TRUE);
Camera cam;
bool running = true;
Uint64 last = SDL_GetPerformanceCounter();
uint8_t selectedBlock = GRASS;
bool meshDirty = false;
std::cout << "=== MiniCraft Controls ===\n"
<< "WASD - Move\n"
<< "Space - Jump\n"
<< "Mouse - Look\n"
<< "LMB - Destroy block\n"
<< "RMB - Place block\n"
<< "1-7 - Select block type\n"
<< "ESC - Quit\n";
while (running) {
Uint64 now = SDL_GetPerformanceCounter();
float dt = (float)(now - last) / SDL_GetPerformanceFrequency();
if (dt > 0.1f) dt = 0.1f;
last = now;
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) running = false;
if (e.type == SDL_KEYDOWN) {
if (e.key.keysym.sym == SDLK_ESCAPE) running = false;
if (e.key.keysym.sym == SDLK_1) selectedBlock = GRASS;
if (e.key.keysym.sym == SDLK_2) selectedBlock = DIRT;
if (e.key.keysym.sym == SDLK_3) selectedBlock = STONE;
if (e.key.keysym.sym == SDLK_4) selectedBlock = WOOD;
if (e.key.keysym.sym == SDLK_5) selectedBlock = LEAVES;
if (e.key.keysym.sym == SDLK_6) selectedBlock = SAND;
if (e.key.keysym.sym == SDLK_7) selectedBlock = WATER;
}
if (e.type == SDL_MOUSEMOTION) {
cam.yaw += e.motion.xrel * MOUSE_SENS;
cam.pitch -= e.motion.yrel * MOUSE_SENS;
if (cam.pitch > 89.0f) cam.pitch = 89.0f;
if (cam.pitch < -89.0f) cam.pitch = -89.0f;
}
if (e.type == SDL_MOUSEBUTTONDOWN) {
glm::ivec3 hit, prev;
if (raycast(cam, hit, prev)) {
if (e.button.button == SDL_BUTTON_LEFT) {
WORLD[hit.x][hit.y][hit.z] = AIR;
meshDirty = true;
} else if (e.button.button == SDL_BUTTON_RIGHT) {
if (prev.x>=0&&prev.x<CHUNK_SIZE&&prev.y>=0&&prev.y<WORLD_H&&prev.z>=0&&prev.z<CHUNK_SIZE)
WORLD[prev.x][prev.y][prev.z] = selectedBlock;
meshDirty = true;
}
}
}
}
if (meshDirty) {
buildMesh(verts, idx);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, verts.size()*sizeof(Vertex), verts.data(), GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size()*sizeof(GLuint), idx.data(), GL_DYNAMIC_DRAW);
meshDirty = false;
}
// Movement
const Uint8* keys = SDL_GetKeyboardState(nullptr);
glm::vec3 fwd = cam.forward(); fwd.y = 0; if (glm::length(fwd)>0) fwd=glm::normalize(fwd);
glm::vec3 rgt = cam.right(); rgt.y = 0; if (glm::length(rgt)>0) rgt=glm::normalize(rgt);
glm::vec3 move{0,0,0};
if (keys[SDL_SCANCODE_W]) move += fwd * MOVE_SPEED;
if (keys[SDL_SCANCODE_S]) move -= fwd * MOVE_SPEED;
if (keys[SDL_SCANCODE_D]) move += rgt * MOVE_SPEED;
if (keys[SDL_SCANCODE_A]) move -= rgt * MOVE_SPEED;
if (keys[SDL_SCANCODE_SPACE] && cam.onGround) cam.vel.y = JUMP_VEL;
moveCamera(cam, move, dt);
// Render
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(prog);
glm::mat4 mvp = proj * cam.view();
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,(GLsizei)idx.size(),GL_UNSIGNED_INT,0);
SDL_GL_SwapWindow(win);
}
glDeleteVertexArrays(1,&VAO);
glDeleteBuffers(1,&VBO);
glDeleteBuffers(1,&EBO);
glDeleteProgram(prog);
SDL_GL_DeleteContext(ctx);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}