[Add] Initial terrain generation.

This commit is contained in:
Ritchie Cunningham 2025-09-16 20:49:00 +01:00
parent 506356458a
commit 7ba0b348bc
19 changed files with 333 additions and 9 deletions

View File

@ -0,0 +1,18 @@
#pragma once
#include <vector>
namespace BettolaLib {
namespace Game {
const int CHUNK_WIDTH = 32;
const int CHUNK_HEIGHT = 32;
struct Chunk {
std::vector<float> heightmap;
Chunk(void) : heightmap(CHUNK_WIDTH * CHUNK_HEIGHT) {}
};
} /* namespace Game. */
} /* namespace BettolaLib. */

View File

@ -0,0 +1,14 @@
#pragma once
#include "bettola/game/chunk.h"
namespace BettolaLib {
namespace Network {
struct ChunkMessage {
int chunk_x;
int chunk_z;
float heightmap[BettolaLib::Game::CHUNK_WIDTH * BettolaLib::Game::CHUNK_HEIGHT];
};
} /* namespace Network. */
} /* namespace BettolaLib. */

View File

@ -8,6 +8,7 @@ enum class MessageType : unsigned char {
PlayerState,
GameState,
PlayerId,
ChunkData,
};
struct MessageHeader {

View File

@ -126,7 +126,7 @@ void Bettola::update(double dt) {
void Bettola::render(void) {
_renderer.render(_camera, _game_client.get_player(),
_game_client.get_remote_players());
_game_client.get_remote_players(), _game_client.get_world());
SDL_GL_SwapWindow(_window);
}

23
src/game/world.cpp Normal file
View File

@ -0,0 +1,23 @@
#include "world.h"
#include <cstring>
#include "bettola/game/chunk.h"
#include "graphics/chunk_mesh.h"
World::~World(void) {
for(auto const& [pos, mesh] : _chunk_meshes) {
delete mesh;
}
}
void World::add_chunk(int chunk_x, int chunk_z, const float* heightmap) {
ChunkPos pos = { chunk_x, chunk_z };
if(_chunk_meshes.count(pos)) {
/* TODO: Chunk already exists, perhaps update it later.. */
return;
}
BettolaLib::Game::Chunk chunk;
memcpy(chunk.heightmap.data(), heightmap, chunk.heightmap.size()*sizeof(float));
_chunk_meshes[pos] = new ChunkMesh(chunk_x, chunk_z, chunk);
}

28
src/game/world.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <map>
#include "graphics/chunk_mesh.h"
/* Allow the use of a pair of ints as a map key. */
struct ChunkPos {
int x, z;
bool operator<(const ChunkPos& other) const {
if(x < other.x) return true;
if(x > other.x) return false;
return z < other.z;
}
};
class World {
public:
~World(void);
void add_chunk(int chunk_x, int chunk_z, const float* heightmap);
const std::map<ChunkPos, ChunkMesh*>& get_chunk_meshes(void) const {
return _chunk_meshes;
}
private:
std::map<ChunkPos, ChunkMesh*> _chunk_meshes;
};

View File

@ -9,6 +9,7 @@
#include "bettola/game/player_base.h"
#include "game/remote_player.h"
#include "bettola/math/vec3.h"
#include "bettola/network/chunk_message.h"
#include "bettola/network/game_state_message.h"
#include "bettola/network/message.h"
@ -58,12 +59,35 @@ void GameClient::process_network_messages(void) {
struct timeval tv = {0,0}; /* Non-blocking. */
if(select(max_fd+1, &read_fds, nullptr, nullptr, &tv) > 0) {
if(FD_ISSET(tcp_fd, &read_fds) && _our_player_id == 0) {
BettolaLib::Network::MessageHeader header;
if(_tcp_socket.recv(&header, sizeof(header)) > 0 && header.type ==
BettolaLib::Network::MessageType::PlayerId) {
_tcp_socket.recv(&_our_player_id, sizeof(_our_player_id));
printf("BetollaClient: Assigned player ID: %u\n", _our_player_id);
if(FD_ISSET(tcp_fd, &read_fds)) {
/* Drain all data from the TCP socket. */
while(true) {
BettolaLib::Network::MessageHeader header;
ssize_t header_bytes = _tcp_socket.recv(&header, sizeof(header));
if(header_bytes <= 0) {
/* No more data, or an error? */
break;
}
if(header.type == BettolaLib::Network::MessageType::PlayerId) {
if(header.size == sizeof(unsigned int)) {
_tcp_socket.recv(&_our_player_id, sizeof(_our_player_id));
printf("BettolaClient: Assigned player ID %u\n", _our_player_id);
}
} else if(header.type == BettolaLib::Network::MessageType::ChunkData) {
if(header.size == sizeof(BettolaLib::Network::ChunkMessage)) {
BettolaLib::Network::ChunkMessage msg;
_tcp_socket.recv(&msg, sizeof(msg));
_world.add_chunk(msg.chunk_x, msg.chunk_z, msg.heightmap);
}
} else {
/* Discard message payloads we don't understand to prevent de-sync. */
char discard_buffer[4096];
if(header.size > 0 && header.size < sizeof(discard_buffer)) {
_tcp_socket.recv(discard_buffer, header.size);
}
}
}
}
if(FD_ISSET(udp_fd, &read_fds)) {
@ -178,3 +202,7 @@ void GameClient::update_players(float dt) {
}
}
const World& GameClient::get_world(void) const {
return _world;
}

View File

@ -6,6 +6,7 @@
#include "game/player.h"
#include "game/remote_player.h"
#include "game/world.h"
#include "graphics/camera.h"
#include "bettola/network/tcpsocket.h"
#include "bettola/network/udpsocket.h"
@ -25,6 +26,8 @@ public:
const std::vector<RemotePlayer>& get_remote_players(void) const { return _remote_players; }
void update_players(float dt);
const World& get_world(void) const;
private:
void _process_udp_messages(void);
void _process_game_state(const BettolaLib::Network::GameStateMessage& msg);
@ -39,4 +42,6 @@ private:
unsigned int _input_sequence_number;
std::deque<BettolaLib::Network::PlayerInputMessage> _pending_inputs;
World _world;
};

View File

@ -0,0 +1,69 @@
#include <GL/glew.h>
#include "bettola/game/chunk.h"
#include "chunk_mesh.h"
ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& chunk) {
/* Generate vertices from the heightmap. */
for(int z = 0; z < BettolaLib::Game::CHUNK_HEIGHT; ++z) {
for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) {
/* Vertex position. */
_vertices.push_back((float)(chunk_x * (BettolaLib::Game::CHUNK_WIDTH-1) + x));
_vertices.push_back(chunk.heightmap[z * BettolaLib::Game::CHUNK_WIDTH+x] * 5.0f); /* 5x scale height */
_vertices.push_back((float)(chunk_z * (BettolaLib::Game::CHUNK_HEIGHT-1) + z));
}
}
/* Generate indices for the triangles. */
for(int z = 0; z < BettolaLib::Game::CHUNK_HEIGHT-1; ++z) {
for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH-1; ++x) {
int top_left = z * BettolaLib::Game::CHUNK_WIDTH + x;
int top_right = top_left + 1;
int bottom_left = (z+1) * BettolaLib::Game::CHUNK_WIDTH + x;
int bottom_right = bottom_left + 1;
/* First triangle of the quad. */
_indices.push_back(top_left);
_indices.push_back(bottom_left);
_indices.push_back(top_right);
/* Second triangle of the quad. */
_indices.push_back(top_right);
_indices.push_back(bottom_left);
_indices.push_back(bottom_right);
}
}
_setup_mesh();
}
ChunkMesh::~ChunkMesh(void) {
glDeleteVertexArrays(1, &_vao);
glDeleteBuffers(1, &_vbo);
glDeleteBuffers(1, &_ebo);
}
void ChunkMesh::_setup_mesh(void) {
glGenVertexArrays(1, &_vao);
glGenBuffers(1, &_vbo);
glGenBuffers(1, &_ebo);
glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferData(GL_ARRAY_BUFFER, _vertices.size()*sizeof(float), _vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, _indices.size()*sizeof(unsigned int), _indices.data(), GL_STATIC_DRAW);
/* Position attrib. */
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
}
void ChunkMesh::draw(void) {
glBindVertexArray(_vao);
glDrawElements(GL_TRIANGLES, _indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}

20
src/graphics/chunk_mesh.h Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <vector>
#include "bettola//game/chunk.h"
class ChunkMesh {
public:
ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& chunk);
~ChunkMesh(void);
void draw(void);
private:
void _setup_mesh(void);
unsigned int _vao, _vbo, _ebo;
std::vector<float> _vertices;
std::vector<unsigned int> _indices;
};

View File

@ -2,6 +2,7 @@
#include <cstdio>
#include <cmath>
#include "game/player.h"
#include "game/world.h"
#include "graphics/camera.h"
#include <SDL3/SDL_timer.h>
@ -136,7 +137,8 @@ bool Renderer::init(int screen_width, int screen_height) {
}
void Renderer::render(const Camera& camera, const Player& player,
const std::vector<RemotePlayer>& remote_players) {
const std::vector<RemotePlayer>& remote_players,
const World& world) {
glClearColor(0.1f, 0.1f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* Need to clear depth buffer too. */
GL_CHECK_ERROR();
@ -168,6 +170,15 @@ void Renderer::render(const Camera& camera, const Player& player,
/* Set players colour back. */
glUniform3f(color_loc, 0.2f, 0.5f, 0.8f);
/* Render world. */
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); /* Wireframe to see geometry. */
for(auto const& [pos, mesh] : world.get_chunk_meshes()) {
BettolaMath::Mat4 model;
_shader.set_mat4("model", model);
mesh->draw();
}
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); /* Reset to default. */
/* Draw the local player's cube. */
const auto& player_pos = player.get_position();
BettolaMath::Mat4 trans_matrix = BettolaMath::Mat4::translation(player_pos.x,

View File

@ -1,6 +1,7 @@
#pragma once
#include <vector>
#include "game/world.h"
#include "graphics/camera.h"
#include "shader.h"
#include "game/player.h"
@ -14,7 +15,8 @@ public:
bool init(int screen_width, int screen_height);
void render(const Camera& camera, const Player& player,
const std::vector<RemotePlayer>& remote_players);
const std::vector<RemotePlayer>& remote_players,
const World& world);
private:
bool _init_shaders();

View File

@ -1,6 +1,7 @@
#pragma once
#include <string>
#include "bettola/math/mat4.h"
class Shader {
public:
@ -9,6 +10,8 @@ public:
bool load_from_files(const std::string& vert_path, const std::string& frag_path);
void use(void);
void set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const;
unsigned int get_id(void) const { return _program_id; }
private:
bool compile_shader(unsigned int& shader_id, const char* shader_source, int shader_type);

View File

@ -0,0 +1,7 @@
#include <GL/glew.h>
#include "bettola/math/mat4.h"
#include "shader.h"
void Shader::set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const {
glUniformMatrix4fv(glGetUniformLocation(_program_id, name.c_str()), 1, GL_FALSE, matrix.get_ptr());
}

View File

@ -5,12 +5,15 @@
#include "bettola/network/tcpsocket.h"
#include "bettola/network/udpsocket.h"
#include "player.h"
#include "world.h"
class Game {
public:
Player* add_player(BettolaLib::Network::TCPSocket* socket);
void remove_player(unsigned int player_id);
void send_initial_chunks(Player* player);
void process_udp_message(const char* buffer, size_t size, const sockaddr_in& from_addr);
void broadcast_game_state(BettolaLib::Network::UDPSocket& udp_socket);
@ -19,4 +22,5 @@ public:
private:
std::vector<Player*> _players;
World _world;
};

View File

@ -0,0 +1,27 @@
#include <cstring>
#include "bettola/game/chunk.h"
#include "game.h"
#include "bettola/network/chunk_message.h"
#include "bettola/network/message.h"
#include "player.h"
void Game::send_initial_chunks(Player* player) {
BettolaLib::Network::MessageHeader header;
header.type = BettolaLib::Network::MessageType::ChunkData;
header.size = sizeof(BettolaLib::Network::ChunkMessage);
/* Send a 3x3 grid of chunks around the origin. */
for(int x = -1; x <=1; ++x) {
for(int z = -1; z <= 1; ++z) {
BettolaLib::Game::Chunk& chunk = _world.get_chunk(x, z);
BettolaLib::Network::ChunkMessage msg;
msg.chunk_x = x;
msg.chunk_z = z;
memcpy(msg.heightmap, chunk.heightmap.data(), chunk.heightmap.size() * sizeof(float));
player->get_socket().send(&header, sizeof(header));
player->get_socket().send(&msg, sizeof(msg));
}
}
}

34
srv/game/world.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "world.h"
#include "bettola/game/chunk.h"
#include "bettola/noise/fast_noise_lite.h"
World::World(void) {
m_noise.SetNoiseType(FastNoiseLite::NoiseType_Perlin);
m_noise.SetFrequency(0.02f);
}
BettolaLib::Game::Chunk& World::get_chunk(int x, int z) {
ChunkPos pos = {x, z};
auto it = m_chunks.find(pos);
if(it != m_chunks.end()) {
return it->second;
}
/* If chunk doesn't exist, generate it. */
BettolaLib::Game::Chunk& new_chunk = m_chunks[pos];
_generate_chunk(new_chunk, x, z);
return new_chunk;
}
void World::_generate_chunk(BettolaLib::Game::Chunk& chunk, int chunk_x, int chunk_z) {
for(int z = 0; z < BettolaLib::Game::CHUNK_HEIGHT; ++z) {
for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) {
/* Calculate world coordinates. */
float world_x = (float)(chunk_x * BettolaLib::Game::CHUNK_WIDTH + x);
float world_z = (float)(chunk_z * BettolaLib::Game::CHUNK_HEIGHT + z);
/* generate noise value. */
chunk.heightmap[z * BettolaLib::Game::CHUNK_WIDTH + x] = m_noise.GetNoise(world_x, world_z);
}
}
}

28
srv/game/world.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <map>
#include "bettola/noise/fast_noise_lite.h"
#include "bettola/game/chunk.h"
struct ChunkPos {
int x, z;
bool operator<(const ChunkPos& other) const {
if(x < other.x) return true;
if(x > other.x) return false;
return z < other.z;
}
};
class World {
public:
World(void);
BettolaLib::Game::Chunk& get_chunk(int x, int z);
private:
void _generate_chunk(BettolaLib::Game::Chunk& chunk, int chunk_x, int chunk_z);
FastNoiseLite m_noise;
std::map<ChunkPos, BettolaLib::Game::Chunk> m_chunks;
};

View File

@ -112,6 +112,8 @@ int main(void) {
unsigned int id = new_player->get_id();
client_socket->send(&id, sizeof(id));
game.send_initial_chunks(new_player);
int client_flags = fcntl(client_socket->get_sockfd(), F_GETFL, 0);
fcntl(client_socket->get_sockfd(), F_SETFL, client_flags | O_NONBLOCK);