From 7ba0b348bc1de7cb127c56195aaa5e8a8d36f713 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Tue, 16 Sep 2025 20:49:00 +0100 Subject: [PATCH] [Add] Initial terrain generation. --- libbettola/include/bettola/game/chunk.h | 18 +++++ .../include/bettola/network/chunk_message.h | 14 ++++ libbettola/include/bettola/network/message.h | 1 + src/bettola.cpp | 2 +- src/game/world.cpp | 23 +++++++ src/game/world.h | 28 ++++++++ src/game_client.cpp | 40 +++++++++-- src/game_client.h | 5 ++ src/graphics/chunk_mesh.cpp | 69 +++++++++++++++++++ src/graphics/chunk_mesh.h | 20 ++++++ src/graphics/renderer.cpp | 13 +++- src/graphics/renderer.h | 4 +- src/graphics/shader.h | 3 + src/graphics/shader_uniforms.cpp | 7 ++ srv/game/game.h | 4 ++ srv/game/game_send_chunks.cpp | 27 ++++++++ srv/game/world.cpp | 34 +++++++++ srv/game/world.h | 28 ++++++++ srv/main.cpp | 2 + 19 files changed, 333 insertions(+), 9 deletions(-) create mode 100644 libbettola/include/bettola/game/chunk.h create mode 100644 libbettola/include/bettola/network/chunk_message.h create mode 100644 src/game/world.cpp create mode 100644 src/game/world.h create mode 100644 src/graphics/chunk_mesh.cpp create mode 100644 src/graphics/chunk_mesh.h create mode 100644 src/graphics/shader_uniforms.cpp create mode 100644 srv/game/game_send_chunks.cpp create mode 100644 srv/game/world.cpp create mode 100644 srv/game/world.h diff --git a/libbettola/include/bettola/game/chunk.h b/libbettola/include/bettola/game/chunk.h new file mode 100644 index 0000000..6778aca --- /dev/null +++ b/libbettola/include/bettola/game/chunk.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace BettolaLib { +namespace Game { + +const int CHUNK_WIDTH = 32; +const int CHUNK_HEIGHT = 32; + +struct Chunk { + std::vector heightmap; + + Chunk(void) : heightmap(CHUNK_WIDTH * CHUNK_HEIGHT) {} +}; + +} /* namespace Game. */ +} /* namespace BettolaLib. */ diff --git a/libbettola/include/bettola/network/chunk_message.h b/libbettola/include/bettola/network/chunk_message.h new file mode 100644 index 0000000..ddd832d --- /dev/null +++ b/libbettola/include/bettola/network/chunk_message.h @@ -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. */ diff --git a/libbettola/include/bettola/network/message.h b/libbettola/include/bettola/network/message.h index ee881c2..bd82ca2 100644 --- a/libbettola/include/bettola/network/message.h +++ b/libbettola/include/bettola/network/message.h @@ -8,6 +8,7 @@ enum class MessageType : unsigned char { PlayerState, GameState, PlayerId, + ChunkData, }; struct MessageHeader { diff --git a/src/bettola.cpp b/src/bettola.cpp index 0f05386..f141241 100644 --- a/src/bettola.cpp +++ b/src/bettola.cpp @@ -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); } diff --git a/src/game/world.cpp b/src/game/world.cpp new file mode 100644 index 0000000..84dbf90 --- /dev/null +++ b/src/game/world.cpp @@ -0,0 +1,23 @@ +#include "world.h" +#include +#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); +} diff --git a/src/game/world.h b/src/game/world.h new file mode 100644 index 0000000..d4562c1 --- /dev/null +++ b/src/game/world.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#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& get_chunk_meshes(void) const { + return _chunk_meshes; + } + +private: + std::map _chunk_meshes; +}; diff --git a/src/game_client.cpp b/src/game_client.cpp index 9e570e5..6be57ca 100644 --- a/src/game_client.cpp +++ b/src/game_client.cpp @@ -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; +} + diff --git a/src/game_client.h b/src/game_client.h index 8350a3f..e896184 100644 --- a/src/game_client.h +++ b/src/game_client.h @@ -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& 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 _pending_inputs; + + World _world; }; diff --git a/src/graphics/chunk_mesh.cpp b/src/graphics/chunk_mesh.cpp new file mode 100644 index 0000000..6289eb9 --- /dev/null +++ b/src/graphics/chunk_mesh.cpp @@ -0,0 +1,69 @@ +#include +#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); +} diff --git a/src/graphics/chunk_mesh.h b/src/graphics/chunk_mesh.h new file mode 100644 index 0000000..da5bd4f --- /dev/null +++ b/src/graphics/chunk_mesh.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#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 _vertices; + std::vector _indices; +}; diff --git a/src/graphics/renderer.cpp b/src/graphics/renderer.cpp index 0576038..8806825 100644 --- a/src/graphics/renderer.cpp +++ b/src/graphics/renderer.cpp @@ -2,6 +2,7 @@ #include #include #include "game/player.h" +#include "game/world.h" #include "graphics/camera.h" #include @@ -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& remote_players) { + const std::vector& 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, diff --git a/src/graphics/renderer.h b/src/graphics/renderer.h index e5cd408..f35e9c0 100644 --- a/src/graphics/renderer.h +++ b/src/graphics/renderer.h @@ -1,6 +1,7 @@ #pragma once #include +#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& remote_players); + const std::vector& remote_players, + const World& world); private: bool _init_shaders(); diff --git a/src/graphics/shader.h b/src/graphics/shader.h index d3a0bc7..c9d6d3d 100644 --- a/src/graphics/shader.h +++ b/src/graphics/shader.h @@ -1,6 +1,7 @@ #pragma once #include +#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); diff --git a/src/graphics/shader_uniforms.cpp b/src/graphics/shader_uniforms.cpp new file mode 100644 index 0000000..5f5a88c --- /dev/null +++ b/src/graphics/shader_uniforms.cpp @@ -0,0 +1,7 @@ +#include +#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()); +} diff --git a/srv/game/game.h b/srv/game/game.h index f9f0859..0c5340e 100644 --- a/srv/game/game.h +++ b/srv/game/game.h @@ -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 _players; + World _world; }; diff --git a/srv/game/game_send_chunks.cpp b/srv/game/game_send_chunks.cpp new file mode 100644 index 0000000..bee1e47 --- /dev/null +++ b/srv/game/game_send_chunks.cpp @@ -0,0 +1,27 @@ +#include +#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)); + } + } +} diff --git a/srv/game/world.cpp b/srv/game/world.cpp new file mode 100644 index 0000000..ddf4c5e --- /dev/null +++ b/srv/game/world.cpp @@ -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); + } + } +} diff --git a/srv/game/world.h b/srv/game/world.h new file mode 100644 index 0000000..bfd3784 --- /dev/null +++ b/srv/game/world.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#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 m_chunks; +}; + diff --git a/srv/main.cpp b/srv/main.cpp index 85cad3e..0b6cc52 100644 --- a/srv/main.cpp +++ b/srv/main.cpp @@ -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);