Files
minecraftslop/main.cpp
2026-03-16 23:06:48 -03:00

835 lines
36 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <cmath>
#include <cstring>
#include <unordered_map>
#include <memory>
#include <algorithm>
#include <climits>
#include <string>
// Build:
// mkdir build && cd 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 = 64;
static const int RENDER_DIST = 6;
static const float MOVE_SPEED = 6.0f;
static const float MOUSE_SENS = 0.12f;
static const float GRAVITY = -22.0f;
static const float JUMP_VEL = 9.0f;
static const float HW = 0.3f;
static const float PH = 1.8f;
static const int HOTBAR_SIZE = 8;
// ─── Block types ─────────────────────────────────────────────────────────────
enum BlockType : uint8_t { AIR=0, GRASS, DIRT, STONE, WOOD, LEAVES, SAND, SNOW, WATER };
static const int NUM_BLOCK_TYPES = 9;
struct BlockColor { float r,g,b; };
static const BlockColor BLOCK_COLORS[] = {
{0,0,0},
{0.40f,0.72f,0.24f}, // GRASS
{0.55f,0.40f,0.22f}, // DIRT
{0.50f,0.50f,0.50f}, // STONE
{0.45f,0.30f,0.15f}, // WOOD
{0.22f,0.55f,0.18f}, // LEAVES
{0.88f,0.83f,0.52f}, // SAND
{0.92f,0.95f,0.98f}, // SNOW
{0.18f,0.42f,0.88f}, // WATER
};
static const char* BLOCK_NAMES[] = {
"Air","Grass","Dirt","Stone","Wood","Leaves","Sand","Snow","Water"
};
static const float FACE_SHADE[] = {1.0f, 0.5f, 0.8f, 0.7f, 0.65f, 0.65f};
// ─── Perlin Noise ─────────────────────────────────────────────────────────────
static int P[512];
static void initNoise(int seed){
int perm[256]; for(int i=0;i<256;i++) perm[i]=i;
unsigned rng=(unsigned)seed;
for(int i=255;i>0;i--){ rng=rng*1664525u+1013904223u; int j=(rng>>16)%(i+1); std::swap(perm[i],perm[j]); }
for(int i=0;i<512;i++) P[i]=perm[i&255];
}
static float fade(float t){return t*t*t*(t*(t*6-15)+10);}
static float lerp(float a,float b,float t){return a+t*(b-a);}
static float grad(int h,float x,float y){h&=3;float u=(h<2)?x:y,v=(h<2)?y:x;return((h&1)?-u:u)+((h&2)?-v:v);}
static float noise2(float x,float y){
int xi=(int)floorf(x)&255,yi=(int)floorf(y)&255;
float xf=x-floorf(x),yf=y-floorf(y),u=fade(xf),v=fade(yf);
int aa=P[P[xi]+yi],ab=P[P[xi]+yi+1],ba=P[P[xi+1]+yi],bb=P[P[xi+1]+yi+1];
return lerp(lerp(grad(aa,xf,yf),grad(ba,xf-1,yf),u),lerp(grad(ab,xf,yf-1),grad(bb,xf-1,yf-1),u),v);
}
static float fbm(float x,float y,int oct=6){
float val=0,amp=1,freq=1,max=0;
for(int i=0;i<oct;i++){val+=noise2(x*freq,y*freq)*amp;max+=amp;amp*=0.5f;freq*=2.0f;}
return val/max;
}
// ─── World chunk ─────────────────────────────────────────────────────────────
struct Chunk {
int cx,cz;
uint8_t blocks[CHUNK_SIZE][WORLD_H][CHUNK_SIZE];
GLuint VAO=0,VBO=0,EBO=0;
int indexCount=0;
bool meshDirty=true,generated=false;
Chunk(int cx,int cz):cx(cx),cz(cz){memset(blocks,AIR,sizeof(blocks));}
~Chunk(){if(VAO){glDeleteVertexArrays(1,&VAO);glDeleteBuffers(1,&VBO);glDeleteBuffers(1,&EBO);}}
};
struct ChunkKey{int x,z;bool operator==(const ChunkKey& o)const{return x==o.x&&z==o.z;}};
struct ChunkKeyHash{size_t operator()(const ChunkKey& k)const{return std::hash<long long>()((long long)k.x<<32|(unsigned)k.z);}};
using ChunkMap=std::unordered_map<ChunkKey,std::unique_ptr<Chunk>,ChunkKeyHash>;
static ChunkMap CHUNKS;
static void generateChunk(Chunk& c){
int wx0=c.cx*CHUNK_SIZE,wz0=c.cz*CHUNK_SIZE;
for(int x=0;x<CHUNK_SIZE;x++)
for(int z=0;z<CHUNK_SIZE;z++){
float wx=(wx0+x)*0.008f,wz=(wz0+z)*0.008f;
float cont=fbm(wx*0.4f,wz*0.4f,4);
float detail=fbm(wx*2.0f+5.3f,wz*2.0f+1.7f,5);
float ridge=1.0f-fabsf(fbm(wx*0.8f+3.1f,wz*0.8f+8.9f,4));
float h=12.0f+cont*18.0f+detail*6.0f+ridge*14.0f*std::max(0.0f,cont);
int top=(int)h; top=std::max(2,std::min(WORLD_H-2,top));
bool isSand=(top<16),isSnow=(top>38);
for(int y=0;y<WORLD_H;y++){
if(y==0) c.blocks[x][y][z]=STONE;
else if(y<top-4) c.blocks[x][y][z]=STONE;
else if(y<top) c.blocks[x][y][z]=isSand?SAND:DIRT;
else if(y==top){ c.blocks[x][y][z]=isSand?SAND:(isSnow?SNOW:GRASS); }
else if(y<=14) c.blocks[x][y][z]=WATER;
else c.blocks[x][y][z]=AIR;
}
if(!isSand&&!isSnow&&top>=16&&top<36&&top+6<WORLD_H){
unsigned col=(unsigned)((wx0+x)*73856093u^(unsigned)(wz0+z)*19349663u);
if((col&0xFF)<18){
int trunk=top+1;
for(int t=trunk;t<trunk+4&&t<WORLD_H;t++) c.blocks[x][t][z]=WOOD;
for(int dx=-2;dx<=2;dx++) for(int dz=-2;dz<=2;dz++) for(int dy=trunk+2;dy<=trunk+5;dy++){
int nx=x+dx,nz=z+dz;
if(nx>=0&&nx<CHUNK_SIZE&&nz>=0&&nz<CHUNK_SIZE&&dy<WORLD_H)
if(c.blocks[nx][dy][nz]==AIR) c.blocks[nx][dy][nz]=LEAVES;
}
}
}
}
c.generated=true;
}
static uint8_t getBlock(int wx,int wy,int wz){
if(wy<0) return STONE; if(wy>=WORLD_H) return AIR;
int cx=(int)floorf((float)wx/CHUNK_SIZE),cz=(int)floorf((float)wz/CHUNK_SIZE);
auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return STONE;
return it->second->blocks[wx-cx*CHUNK_SIZE][wy][wz-cz*CHUNK_SIZE];
}
static void setBlock(int wx,int wy,int wz,uint8_t val){
if(wy<0||wy>=WORLD_H) return;
int cx=(int)floorf((float)wx/CHUNK_SIZE),cz=(int)floorf((float)wz/CHUNK_SIZE);
auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return;
int lx=wx-cx*CHUNK_SIZE,lz=wz-cz*CHUNK_SIZE;
it->second->blocks[lx][wy][lz]=val; it->second->meshDirty=true;
if(lx==0){auto n=CHUNKS.find({cx-1,cz});if(n!=CHUNKS.end())n->second->meshDirty=true;}
if(lx==CHUNK_SIZE-1){auto n=CHUNKS.find({cx+1,cz});if(n!=CHUNKS.end())n->second->meshDirty=true;}
if(lz==0){auto n=CHUNKS.find({cx,cz-1});if(n!=CHUNKS.end())n->second->meshDirty=true;}
if(lz==CHUNK_SIZE-1){auto n=CHUNKS.find({cx,cz+1});if(n!=CHUNKS.end())n->second->meshDirty=true;}
}
// ─── World mesh ──────────────────────────────────────────────────────────────
struct Vertex{float x,y,z,r,g,b;};
static bool shouldDrawFace(uint8_t cur,int wx,int wy,int wz){
uint8_t nb=getBlock(wx,wy,wz);
if(nb==AIR) return true;
if(nb==WATER||nb==LEAVES) return cur!=nb;
return false;
}
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 buildChunkMesh(Chunk& c){
std::vector<Vertex> verts; std::vector<GLuint> idx;
int wx0=c.cx*CHUNK_SIZE,wz0=c.cz*CHUNK_SIZE;
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=c.blocks[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=wx0+x,by=y,bz=wz0+z; int wx=wx0+x,wz=wz0+z;
if(shouldDrawFace(b,wx,y+1,wz)) 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]);
if(shouldDrawFace(b,wx,y-1,wz)) 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]);
if(shouldDrawFace(b,wx,y,wz+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]);
if(shouldDrawFace(b,wx,y,wz-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]);
if(shouldDrawFace(b,wx-1,y,wz)) 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]);
if(shouldDrawFace(b,wx+1,y,wz)) 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]);
}
if(!c.VAO){glGenVertexArrays(1,&c.VAO);glGenBuffers(1,&c.VBO);glGenBuffers(1,&c.EBO);}
glBindVertexArray(c.VAO);
glBindBuffer(GL_ARRAY_BUFFER,c.VBO); glBufferData(GL_ARRAY_BUFFER,verts.size()*sizeof(Vertex),verts.data(),GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,c.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);
c.indexCount=(int)idx.size(); c.meshDirty=false;
}
// ─── 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; out float vFog;
uniform mat4 uMVP; uniform vec3 uCamPos;
void main(){
gl_Position=uMVP*vec4(aPos,1.0); vColor=aColor;
float dist=length(aPos-uCamPos);
vFog=clamp((dist-60.0)/40.0,0.0,1.0);
})";
static const char* FS_SRC=R"(
#version 330 core
in vec3 vColor; in float vFog; out vec4 fragColor;
void main(){
vec3 fogColor=vec3(0.53,0.81,0.98);
fragColor=vec4(mix(vColor,fogColor,vFog),1.0);
})";
static const char* VS_UI=R"(
#version 330 core
layout(location=0) in vec2 aPos;
layout(location=1) in vec3 aColor;
out vec3 vColor;
uniform mat4 uProj;
void main(){ gl_Position=uProj*vec4(aPos,0.0,1.0); vColor=aColor; })";
static const char* FS_UI=R"(
#version 330 core
in vec3 vColor; out vec4 fragColor;
uniform float uAlpha;
void main(){ fragColor=vec4(vColor,uAlpha); })";
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 err: "<<buf<<"\n";}
return s;
}
static GLuint buildProg(const char* vs,const char* fs){
GLuint v=compileShader(GL_VERTEX_SHADER,vs),f=compileShader(GL_FRAGMENT_SHADER,fs);
GLuint p=glCreateProgram(); glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p);
glDeleteShader(v); glDeleteShader(f); return p;
}
// ─── 2D UI Renderer ──────────────────────────────────────────────────────────
struct UIRenderer {
GLuint prog=0,VAO=0,VBO=0;
GLint projLoc=-1,alphaLoc=-1;
glm::mat4 proj{1.0f};
void init(){
prog=buildProg(VS_UI,FS_UI);
projLoc=glGetUniformLocation(prog,"uProj");
alphaLoc=glGetUniformLocation(prog,"uAlpha");
proj=glm::ortho(0.0f,(float)SCREEN_W,(float)SCREEN_H,0.0f);
glGenVertexArrays(1,&VAO);
glGenBuffers(1,&VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,1024*sizeof(float),nullptr,GL_DYNAMIC_DRAW);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
}
void begin(){
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glUseProgram(prog);
glUniformMatrix4fv(projLoc,1,GL_FALSE,glm::value_ptr(proj));
glBindVertexArray(VAO);
}
void end(){
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glBindVertexArray(0);
}
void rect(float x,float y,float w,float h,float r,float g,float b,float a=1.0f){
float verts[]={
x, y, r,g,b,
x+w, y, r,g,b,
x+w, y+h, r,g,b,
x, y, r,g,b,
x+w, y+h, r,g,b,
x, y+h, r,g,b
};
glUniform1f(alphaLoc,a);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferSubData(GL_ARRAY_BUFFER,0,sizeof(verts),verts);
glDrawArrays(GL_TRIANGLES,0,6);
}
void outline(float x,float y,float w,float h,float r,float g,float b,float t=2.0f){
rect(x,y,w,t,r,g,b);
rect(x,y+h-t,w,t,r,g,b);
rect(x,y,t,h,r,g,b);
rect(x+w-t,y,t,h,r,g,b);
}
};
// ─── Global UI + PlayerModel forward declarations ────────────────────────────
// Declared here so drawText / drawHUD / drawInventory can use them.
static UIRenderer UI;
// ─── Tiny bitmap font (5×7 per glyph, ASCII 32126) ─────────────────────────
static const uint8_t FONT5x7[][5] = {
{0x00,0x00,0x00,0x00,0x00}, // space
{0x00,0x00,0x5F,0x00,0x00}, // !
{0x00,0x07,0x00,0x07,0x00}, // "
{0x14,0x7F,0x14,0x7F,0x14}, // #
{0x24,0x2A,0x7F,0x2A,0x12}, // $
{0x23,0x13,0x08,0x64,0x62}, // %
{0x36,0x49,0x55,0x22,0x50}, // &
{0x00,0x05,0x03,0x00,0x00}, // '
{0x00,0x1C,0x22,0x41,0x00}, // (
{0x00,0x41,0x22,0x1C,0x00}, // )
{0x14,0x08,0x3E,0x08,0x14}, // *
{0x08,0x08,0x3E,0x08,0x08}, // +
{0x00,0x50,0x30,0x00,0x00}, // ,
{0x08,0x08,0x08,0x08,0x08}, // -
{0x00,0x60,0x60,0x00,0x00}, // .
{0x20,0x10,0x08,0x04,0x02}, // /
{0x3E,0x51,0x49,0x45,0x3E}, // 0
{0x00,0x42,0x7F,0x40,0x00}, // 1
{0x42,0x61,0x51,0x49,0x46}, // 2
{0x21,0x41,0x45,0x4B,0x31}, // 3
{0x18,0x14,0x12,0x7F,0x10}, // 4
{0x27,0x45,0x45,0x45,0x39}, // 5
{0x3C,0x4A,0x49,0x49,0x30}, // 6
{0x01,0x71,0x09,0x05,0x03}, // 7
{0x36,0x49,0x49,0x49,0x36}, // 8
{0x06,0x49,0x49,0x29,0x1E}, // 9
{0x00,0x36,0x36,0x00,0x00}, // :
{0x00,0x56,0x36,0x00,0x00}, // ;
{0x08,0x14,0x22,0x41,0x00}, // <
{0x14,0x14,0x14,0x14,0x14}, // =
{0x00,0x41,0x22,0x14,0x08}, // >
{0x02,0x01,0x51,0x09,0x06}, // ?
{0x32,0x49,0x79,0x41,0x3E}, // @
{0x7E,0x11,0x11,0x11,0x7E}, // A
{0x7F,0x49,0x49,0x49,0x36}, // B
{0x3E,0x41,0x41,0x41,0x22}, // C
{0x7F,0x41,0x41,0x22,0x1C}, // D
{0x7F,0x49,0x49,0x49,0x41}, // E
{0x7F,0x09,0x09,0x09,0x01}, // F
{0x3E,0x41,0x49,0x49,0x7A}, // G
{0x7F,0x08,0x08,0x08,0x7F}, // H
{0x00,0x41,0x7F,0x41,0x00}, // I
{0x20,0x40,0x41,0x3F,0x01}, // J
{0x7F,0x08,0x14,0x22,0x41}, // K
{0x7F,0x40,0x40,0x40,0x40}, // L
{0x7F,0x02,0x04,0x02,0x7F}, // M
{0x7F,0x04,0x08,0x10,0x7F}, // N
{0x3E,0x41,0x41,0x41,0x3E}, // O
{0x7F,0x09,0x09,0x09,0x06}, // P
{0x3E,0x41,0x51,0x21,0x5E}, // Q
{0x7F,0x09,0x19,0x29,0x46}, // R
{0x46,0x49,0x49,0x49,0x31}, // S
{0x01,0x01,0x7F,0x01,0x01}, // T
{0x3F,0x40,0x40,0x40,0x3F}, // U
{0x1F,0x20,0x40,0x20,0x1F}, // V
{0x3F,0x40,0x38,0x40,0x3F}, // W
{0x63,0x14,0x08,0x14,0x63}, // X
{0x07,0x08,0x70,0x08,0x07}, // Y
{0x61,0x51,0x49,0x45,0x43}, // Z
{0x00,0x7F,0x41,0x41,0x00}, // [
{0x02,0x04,0x08,0x10,0x20}, // backslash
{0x00,0x41,0x41,0x7F,0x00}, // ]
{0x04,0x02,0x01,0x02,0x04}, // ^
{0x40,0x40,0x40,0x40,0x40}, // _
{0x00,0x01,0x02,0x04,0x00}, // `
{0x20,0x54,0x54,0x54,0x78}, // a
{0x7F,0x48,0x44,0x44,0x38}, // b
{0x38,0x44,0x44,0x44,0x20}, // c
{0x38,0x44,0x44,0x48,0x7F}, // d
{0x38,0x54,0x54,0x54,0x18}, // e
{0x08,0x7E,0x09,0x01,0x02}, // f
{0x0C,0x52,0x52,0x52,0x3E}, // g
{0x7F,0x08,0x04,0x04,0x78}, // h
{0x00,0x44,0x7D,0x40,0x00}, // i
{0x20,0x40,0x44,0x3D,0x00}, // j
{0x7F,0x10,0x28,0x44,0x00}, // k
{0x00,0x41,0x7F,0x40,0x00}, // l
{0x7C,0x04,0x18,0x04,0x78}, // m
{0x7C,0x08,0x04,0x04,0x78}, // n
{0x38,0x44,0x44,0x44,0x38}, // o
{0x7C,0x14,0x14,0x14,0x08}, // p
{0x08,0x14,0x14,0x18,0x7C}, // q
{0x7C,0x08,0x04,0x04,0x08}, // r
{0x48,0x54,0x54,0x54,0x20}, // s
{0x04,0x3F,0x44,0x40,0x20}, // t
{0x3C,0x40,0x40,0x20,0x7C}, // u
{0x1C,0x20,0x40,0x20,0x1C}, // v
{0x3C,0x40,0x30,0x40,0x3C}, // w
{0x44,0x28,0x10,0x28,0x44}, // x
{0x0C,0x50,0x50,0x50,0x3C}, // y
{0x44,0x64,0x54,0x4C,0x44}, // z
{0x00,0x08,0x36,0x41,0x00}, // {
{0x00,0x00,0x7F,0x00,0x00}, // |
{0x00,0x41,0x36,0x08,0x00}, // }
{0x10,0x08,0x08,0x10,0x08}, // ~
};
static void drawText(const char* text,float x,float y,float scale,
float r,float g,float b,float a=1.0f){
float cx=x;
for(int i=0;text[i];i++){
int c=text[i]-32;
if(c<0||c>=(int)(sizeof(FONT5x7)/sizeof(FONT5x7[0]))){cx+=(6*scale);continue;}
for(int col=0;col<5;col++){
uint8_t bits=FONT5x7[c][col];
for(int row=0;row<7;row++){
if(bits&(1<<row))
UI.rect(cx+col*scale,y+row*scale,scale,scale,r,g,b,a);
}
}
cx+=6*scale;
}
}
// ─── Inventory ───────────────────────────────────────────────────────────────
struct Inventory {
uint8_t hotbar[HOTBAR_SIZE]={GRASS,DIRT,STONE,WOOD,LEAVES,SAND,SNOW,WATER};
int selected=0;
bool open=false;
uint8_t selectedBlock()const{return hotbar[selected];}
};
static void drawInventory(Inventory& inv){
const float SLOT=52.0f;
const float GAP=6.0f;
const float BORDER=4.0f;
int n=HOTBAR_SIZE;
float totalW=n*(SLOT+GAP)-GAP;
float hx=(SCREEN_W-totalW)*0.5f;
float hy=SCREEN_H-SLOT-16.0f;
// Background bar
UI.rect(hx-8,hy-8,totalW+16,SLOT+16,0.15f,0.15f,0.15f,0.75f);
UI.outline(hx-8,hy-8,totalW+16,SLOT+16,0.0f,0.0f,0.0f);
for(int i=0;i<n;i++){
float sx=hx+i*(SLOT+GAP),sy=hy;
bool sel=(i==inv.selected);
UI.rect(sx,sy,SLOT,SLOT,sel?0.30f:0.20f,sel?0.30f:0.20f,sel?0.30f:0.20f,0.9f);
uint8_t bt=inv.hotbar[i];
if(bt!=AIR){
auto& bc=BLOCK_COLORS[bt];
UI.rect(sx+BORDER,sy+BORDER,SLOT-2*BORDER,SLOT-2*BORDER-10,bc.r,bc.g,bc.b,1.0f);
UI.rect(sx+BORDER,sy+SLOT-BORDER-10-4,SLOT-2*BORDER,4,bc.r*0.65f,bc.g*0.65f,bc.b*0.65f,1.0f);
}
if(sel) UI.outline(sx,sy,SLOT,SLOT,1.0f,1.0f,1.0f,BORDER*0.5f);
else UI.outline(sx,sy,SLOT,SLOT,0.0f,0.0f,0.0f,2.0f);
char num[3]; snprintf(num,3,"%d",i+1);
drawText(num,sx+4,sy+SLOT-12,1.5f,0.8f,0.8f,0.8f);
}
{
uint8_t bt=inv.selectedBlock();
const char* name=BLOCK_NAMES[bt];
float tw=strlen(name)*6*2.0f;
drawText(name,(SCREEN_W-tw)*0.5f,hy-26,2.0f,1.0f,1.0f,1.0f,0.9f);
}
if(!inv.open) return;
const int COLS=4;
const int ROWS=(NUM_BLOCK_TYPES-1+COLS-1)/COLS;
float iw=COLS*(SLOT+GAP)-GAP+32.0f;
float ih=ROWS*(SLOT+GAP)-GAP+60.0f;
float ix=(SCREEN_W-iw)*0.5f;
float iy=(SCREEN_H-ih)*0.5f;
UI.rect(0,0,(float)SCREEN_W,(float)SCREEN_H,0,0,0,0.45f);
UI.rect(ix,iy,iw,ih,0.18f,0.18f,0.18f,0.95f);
UI.outline(ix,iy,iw,ih,0.6f,0.6f,0.6f,2.0f);
drawText("INVENTORY",ix+10,iy+10,2.0f,1.0f,1.0f,1.0f);
for(int bi=1;bi<NUM_BLOCK_TYPES;bi++){
int row=(bi-1)/COLS,col=(bi-1)%COLS;
float sx=ix+16+col*(SLOT+GAP);
float sy=iy+40+row*(SLOT+GAP);
bool inHotbar=false;
for(int h=0;h<HOTBAR_SIZE;h++) if(inv.hotbar[h]==bi) inHotbar=true;
auto& bc=BLOCK_COLORS[bi];
UI.rect(sx,sy,SLOT,SLOT,0.25f,0.25f,0.25f,0.9f);
UI.rect(sx+BORDER,sy+BORDER,SLOT-2*BORDER,SLOT-2*BORDER-8,bc.r,bc.g,bc.b,1.0f);
UI.rect(sx+BORDER,sy+SLOT-BORDER-8-4,SLOT-2*BORDER,4,bc.r*0.65f,bc.g*0.65f,bc.b*0.65f,1.0f);
UI.outline(sx,sy,SLOT,SLOT,inHotbar?1.0f:0.4f,inHotbar?1.0f:0.4f,inHotbar?0.0f:0.4f,2.0f);
float scale=1.5f;
float tw=strlen(BLOCK_NAMES[bi])*6*scale;
float tx=sx+(SLOT-tw)*0.5f;
drawText(BLOCK_NAMES[bi],tx,sy+SLOT-10,scale,0.9f,0.9f,0.9f);
}
drawText("Click a slot (1-8) to select. E to close.",ix+10,iy+ih-18,1.5f,0.7f,0.7f,0.7f);
}
// ─── HUD ─────────────────────────────────────────────────────────────────────
static void drawHUD(const glm::vec3& pos,uint8_t /*selBlock*/,bool thirdPerson){
float cx=(float)SCREEN_W/2,cy=(float)SCREEN_H/2;
UI.rect(cx-1,cy-10,2,20,1,1,1,0.85f);
UI.rect(cx-10,cy-1,20,2,1,1,1,0.85f);
UI.rect(cx-1,cy-1,2,2,0,0,0,0.85f);
char buf[64];
snprintf(buf,64,"X:%.0f Y:%.0f Z:%.0f",pos.x,pos.y-PH,pos.z);
UI.rect(6,6,strlen(buf)*6*1.5f+6,18,0,0,0,0.5f);
drawText(buf,9,9,1.5f,1.0f,1.0f,1.0f);
if(thirdPerson)
drawText("F5: 3rd Person",9,28,1.5f,1.0f,1.0f,0.6f);
}
// ─── Camera / Player ─────────────────────────────────────────────────────────
struct Camera {
glm::vec3 pos{0,40,0};
float yaw=0,pitch=0;
glm::vec3 vel{0,0,0};
bool onGround=false;
glm::vec3 forward()const{
float y=glm::radians(yaw),p=glm::radians(pitch);
return glm::normalize(glm::vec3(cosf(p)*cosf(y),sinf(p),cosf(p)*sinf(y)));
}
glm::vec3 right()const{return glm::normalize(glm::cross(forward(),{0,1,0}));}
glm::mat4 firstPersonView()const{return glm::lookAt(pos,pos+forward(),{0,1,0});}
glm::mat4 thirdPersonView()const{
glm::vec3 fwd=forward();
glm::vec3 eye=pos-fwd*6.0f+glm::vec3(0,2,0);
return glm::lookAt(eye,pos,{0,1,0});
}
};
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; if(y>=WORLD_H) return false;
uint8_t b=getBlock(x,y,z); return b!=AIR&&b!=WATER&&b!=LEAVES;
}
static bool aabbSolid(float px,float py,float pz){
int x0=(int)floorf(px-HW),x1=(int)floorf(px+HW);
int y0=(int)floorf(py-PH),y1=(int)floorf(py+0.05f);
int z0=(int)floorf(pz-HW),z1=(int)floorf(pz+HW);
for(int x=x0;x<=x1;x++) for(int y=y0;y<=y1;y++) for(int z=z0;z<=z1;z++)
if(solidAt((float)x,(float)y,(float)z)) return true;
return false;
}
static void moveCamera(Camera& cam,const glm::vec3& move,float dt){
cam.pos.x+=move.x*dt; if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)) cam.pos.x-=move.x*dt;
cam.pos.z+=move.z*dt; if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)) cam.pos.z-=move.z*dt;
cam.vel.y+=GRAVITY*dt; cam.pos.y+=cam.vel.y*dt;
if(cam.vel.y<0){
if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){
cam.pos.y=floorf(cam.pos.y-PH)+1.0f+PH+0.001f;
cam.vel.y=0; cam.onGround=true;
} else cam.onGround=false;
} else {
if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){
cam.pos.y=floorf(cam.pos.y+0.05f)-0.05f-0.001f; cam.vel.y=0;
}
cam.onGround=false;
}
}
// ─── Player model (3rd person) ───────────────────────────────────────────────
struct PlayerModel {
GLuint VAO=0,VBO=0,EBO=0;
std::vector<Vertex> verts;
std::vector<GLuint> idx;
int indexCount=0;
void addBox(float x0,float y0,float z0,float x1,float y1,float z1,
float r,float g,float b){
auto face=[&](glm::vec3 v0,glm::vec3 v1,glm::vec3 v2,glm::vec3 v3,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});
};
face({x0,y1,z0},{x1,y1,z0},{x1,y1,z1},{x0,y1,z1},1.0f); // Top
face({x0,y0,z1},{x1,y0,z1},{x1,y0,z0},{x0,y0,z0},0.5f); // Bottom
face({x0,y0,z1},{x1,y0,z1},{x1,y1,z1},{x0,y1,z1},0.8f); // Front
face({x1,y0,z0},{x0,y0,z0},{x0,y1,z0},{x1,y1,z0},0.7f); // Back
face({x0,y0,z0},{x0,y0,z1},{x0,y1,z1},{x0,y1,z0},0.65f); // Left
face({x1,y0,z1},{x1,y0,z0},{x1,y1,z0},{x1,y1,z1},0.65f); // Right
}
void build(){
verts.clear(); idx.clear();
float sk=0.90f,sg_=0.72f,sb_=0.55f;
float tr=0.25f,tg=0.45f,tb=0.75f;
float lr=0.20f,lg=0.20f,lb=0.60f;
float hr=0.30f,hg=0.18f,hb=0.08f;
addBox(-0.3f,1.55f,-0.3f, 0.3f,2.15f,0.3f, sk,sg_,sb_); // Head
addBox(-0.32f,1.9f,-0.32f, 0.32f,2.17f,0.32f, hr,hg,hb); // Hair
addBox(-0.3f,0.7f,-0.2f, 0.3f,1.5f,0.2f, tr,tg,tb); // Body
addBox(-0.55f,0.7f,-0.15f, -0.32f,1.45f,0.15f, sk,sg_,sb_);// Left arm
addBox( 0.32f,0.7f,-0.15f, 0.55f,1.45f,0.15f, sk,sg_,sb_);// Right arm
addBox(-0.28f,0.0f,-0.15f, -0.05f,0.7f,0.15f, lr,lg,lb); // Left leg
addBox( 0.05f,0.0f,-0.15f, 0.28f,0.7f,0.15f, lr,lg,lb); // Right leg
indexCount=(int)idx.size();
if(!VAO){glGenVertexArrays(1,&VAO);glGenBuffers(1,&VBO);glGenBuffers(1,&EBO);}
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,verts.size()*sizeof(Vertex),verts.data(),GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,idx.size()*sizeof(GLuint),idx.data(),GL_STATIC_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);
glBindVertexArray(0);
}
void draw(GLuint prog,GLint mvpLoc,GLint camLoc,
const glm::mat4& proj,const glm::mat4& view,
const Camera& cam) const {
glm::vec3 feet=cam.pos-glm::vec3(0,PH,0);
glm::mat4 model=glm::translate(glm::mat4(1),feet);
model=glm::rotate(model,glm::radians(cam.yaw+180.0f),glm::vec3(0,1,0));
glm::mat4 mvp=proj*view*model;
glUseProgram(prog);
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
glUniform3fv(camLoc,1,glm::value_ptr(cam.pos));
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,(GLsizei)indexCount,GL_UNSIGNED_INT,0);
glBindVertexArray(0);
}
};
static PlayerModel PLAYER_MODEL;
// ─── Raycast ─────────────────────────────────────────────────────────────────
static bool raycast(const Camera& cam,glm::ivec3& hit,glm::ivec3& prev){
glm::vec3 dir=cam.forward(),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)};
if(bp.y<0||bp.y>=WORLD_H){last=bp;continue;}
uint8_t b=getBlock(bp.x,bp.y,bp.z);
if(b!=AIR&&b!=WATER){hit=bp;prev=last;return true;}
last=bp;
}
return false;
}
// ─── Chunk streaming ─────────────────────────────────────────────────────────
static void updateChunks(int pcx,int pcz){
for(int dx=-RENDER_DIST;dx<=RENDER_DIST;dx++)
for(int dz=-RENDER_DIST;dz<=RENDER_DIST;dz++){
ChunkKey k{pcx+dx,pcz+dz};
if(CHUNKS.find(k)==CHUNKS.end()){
auto c=std::make_unique<Chunk>(k.x,k.z); generateChunk(*c);
CHUNKS[k]=std::move(c);
for(auto& [nk,nc]:CHUNKS) if(abs(nk.x-k.x)+abs(nk.z-k.z)==1) nc->meshDirty=true;
}
}
std::vector<ChunkKey> toRemove;
for(auto& [k,c]:CHUNKS) if(abs(k.x-pcx)>RENDER_DIST+1||abs(k.z-pcz)>RENDER_DIST+1) toRemove.push_back(k);
for(auto& k:toRemove) CHUNKS.erase(k);
}
// ─── 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 prog3d=buildProg(VS_SRC,FS_SRC);
GLint mvpLoc=glGetUniformLocation(prog3d,"uMVP");
GLint camLoc=glGetUniformLocation(prog3d,"uCamPos");
UI.init();
initNoise(12345);
std::cout<<"Generating world...\n";
updateChunks(0,0);
for(auto& [k,c]:CHUNKS) if(c->generated) buildChunkMesh(*c);
Camera cam;
for(int y=WORLD_H-1;y>=0;y--){
uint8_t b=getBlock(0,y,0);
if(b!=AIR&&b!=WATER){cam.pos.y=(float)y+1+PH+0.1f;break;}
}
cam.pos.x=0.5f; cam.pos.z=0.5f;
PLAYER_MODEL.build();
glm::mat4 proj=glm::perspective(glm::radians(70.0f),(float)SCREEN_W/SCREEN_H,0.05f,500.0f);
SDL_SetRelativeMouseMode(SDL_TRUE);
Inventory inv;
bool thirdPerson=false;
bool running=true;
Uint64 last=SDL_GetPerformanceCounter();
int lastPcx=INT_MAX,lastPcz=INT_MAX;
std::cout<<"=== MiniCraft ===\n"
<<"WASD Move\n"
<<"Space Jump\n"
<<"Mouse Look\n"
<<"LMB Break block\n"
<<"RMB Place block\n"
<<"1-8 Hotbar slot\n"
<<"E Open/close inventory\n"
<<"F5 Toggle 3rd person\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){
switch(e.key.keysym.sym){
case SDLK_ESCAPE: running=false; break;
case SDLK_e:
inv.open=!inv.open;
SDL_SetRelativeMouseMode(inv.open?SDL_FALSE:SDL_TRUE);
break;
case SDLK_F5: thirdPerson=!thirdPerson; break;
case SDLK_1: inv.selected=0; break;
case SDLK_2: inv.selected=1; break;
case SDLK_3: inv.selected=2; break;
case SDLK_4: inv.selected=3; break;
case SDLK_5: inv.selected=4; break;
case SDLK_6: inv.selected=5; break;
case SDLK_7: inv.selected=6; break;
case SDLK_8: inv.selected=7; break;
}
}
if(e.type==SDL_MOUSEWHEEL){
inv.selected=(inv.selected-e.wheel.y+HOTBAR_SIZE)%HOTBAR_SIZE;
}
if(!inv.open){
if(e.type==SDL_MOUSEMOTION){
cam.yaw+=e.motion.xrel*MOUSE_SENS;
cam.pitch-=e.motion.yrel*MOUSE_SENS;
if(cam.pitch>89)cam.pitch=89; if(cam.pitch<-89)cam.pitch=-89;
}
if(e.type==SDL_MOUSEBUTTONDOWN){
glm::ivec3 hit,prev;
if(raycast(cam,hit,prev)){
if(e.button.button==SDL_BUTTON_LEFT)
setBlock(hit.x,hit.y,hit.z,AIR);
else if(e.button.button==SDL_BUTTON_RIGHT)
setBlock(prev.x,prev.y,prev.z,inv.selectedBlock());
}
}
} else {
if(e.type==SDL_MOUSEBUTTONDOWN&&e.button.button==SDL_BUTTON_LEFT){
const int COLS=4; const float SLOT=52.0f,GAP=6.0f;
float iw=COLS*(SLOT+GAP)-GAP+32.0f;
float ih=((NUM_BLOCK_TYPES-1+COLS-1)/COLS)*(SLOT+GAP)-GAP+60.0f;
float ix=(SCREEN_W-iw)*0.5f,iy=(SCREEN_H-ih)*0.5f;
int mx=e.button.x,my=e.button.y;
for(int bi=1;bi<NUM_BLOCK_TYPES;bi++){
int row=(bi-1)/COLS,col=(bi-1)%COLS;
float sx=ix+16+col*(SLOT+GAP),sy=iy+40+row*(SLOT+GAP);
if(mx>=sx&&mx<=sx+SLOT&&my>=sy&&my<=sy+SLOT){
inv.hotbar[inv.selected]=(uint8_t)bi;
}
}
}
}
}
if(!inv.open){
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);
}
int pcx=(int)floorf(cam.pos.x/CHUNK_SIZE);
int pcz=(int)floorf(cam.pos.z/CHUNK_SIZE);
if(pcx!=lastPcx||pcz!=lastPcz){ updateChunks(pcx,pcz); lastPcx=pcx; lastPcz=pcz; }
int rebuilt=0;
for(auto& [k,c]:CHUNKS)
if(c->meshDirty&&c->generated){buildChunkMesh(*c);if(++rebuilt>=4)break;}
// ── Render 3D world ──
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glm::mat4 view=thirdPerson?cam.thirdPersonView():cam.firstPersonView();
glm::mat4 mvp=proj*view;
glUseProgram(prog3d);
glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp));
glUniform3fv(camLoc,1,glm::value_ptr(cam.pos));
for(auto& [k,c]:CHUNKS)
if(c->indexCount>0){glBindVertexArray(c->VAO);glDrawElements(GL_TRIANGLES,c->indexCount,GL_UNSIGNED_INT,0);}
// ── Player model (3rd person only) ──
if(thirdPerson)
PLAYER_MODEL.draw(prog3d,mvpLoc,camLoc,proj,view,cam);
// ── 2D UI ──
UI.begin();
drawHUD(cam.pos,inv.selectedBlock(),thirdPerson);
drawInventory(inv);
UI.end();
SDL_GL_SwapWindow(win);
}
CHUNKS.clear();
glDeleteProgram(prog3d);
glDeleteProgram(UI.prog);
glDeleteVertexArrays(1,&UI.VAO); glDeleteBuffers(1,&UI.VBO);
glDeleteVertexArrays(1,&PLAYER_MODEL.VAO);
glDeleteBuffers(1,&PLAYER_MODEL.VBO); glDeleteBuffers(1,&PLAYER_MODEL.EBO);
SDL_GL_DeleteContext(ctx);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}