feat: sound (#28)

* build: add OpenAL dependency

* feat(deps): add dr_libs audio library

* feat(audio): integrate OpenAL audio engine with WAV/MP3/FLAC support

* feat: add bgm001

* feat(audio): add audio fade in/out and fix delta time naming

* fix(audio-source): improve state, volume, and const correctness

* feat(audio): add 3D audio playback with source pool and auto-loading

* feat: add ogg loader

* feat(audio): add footstep sounds and fix audio listener orientation

* feat(sound): add block place and break sounds

* feat(audio): add per-channel volume control and config reload

* fix(audio): set distance model, reduce max distance, fix sound pos

- Set OpenAL distance model to AL_INVERSE_DISTANCE_CLAMPED in engine.
- Reduced AL_MAX_DISTANCE from 100 to 48 for better falloff.
- Fixed block sound position to use world coordinates instead of local block coordinates.

* refactor(game_time): introduce Timer for time-based updates and rename existing to TickTimer

Add a new `Timer` class that operates in seconds (using `TimeType`) alongside the existing `TickTimer` which uses `TickType`. Rename the original `Timer` to `TickTimer` throughout the codebase. Update `ClientWorld` to maintain separate maps for tick-based and time-based timers, and replace the hardcoded footstep timer in `ClientPlayer` with a time-based callback timer. Also add `play_2d` to `AudioEngine` and load an ambient bird sound to demonstrate the new timer functionality.

* feat(gameplay): add ocean wave ambient sounds with random selection

Add four ocean wave FLAC sound files and play one randomly when player is within 10 blocks of sea level and in an ocean biome. Include a Random member initialized from ChunkGenerator seed for consistent randomization.

* feat(gameplay): detect underwater state and play transition/bubble sounds

- Add is_underwater() and set_underwater() to ClientPlayer.
- Change get_world() to return non-const reference for audio access.
- Update Camera::update_move_camera() to detect water block changes and trigger transition sound.
- Add repeating timer in ClientWorld to play random bubble ambient sounds while underwater.
- Include new ambient sound assets for water transitions and bubbles.
- Also add sand walk sound asset (unused in this commit).

* feat(assets): add and update block break/place/walk sounds

Add sounds for dirt, grass_block, leaf, log, sand, snowy_grass_block, stone.
Update existing leaf/walk, sand/place, sand/walk, and stone/place sounds.

* feat(gameplay): add walking sound for players and adjust underwater bubble interval

- Move walk/run sound interval constants from anonymous namespace to ClientPlayer header
- Add moving_time field to PlayerInfo to track continuous movement
- Implement play_walk_sound callback triggered by WALK and RUN gaits
- Increase underwater bubble timer from 1.0s to 1.5s

* feat(asset): add grass block break and place sound effects

* feat(renderer): add --no-debug flag to disable OpenGL debug output

* feat(audio): add day/night-based BGM switching

* feat(audio): add underwater low-pass filter effect

* feat(audio): add pitch control to audio sources

Implement set_pitch method that clamps pitch between 0.0 and 1.0 and applies it via AL_PITCH. Includes minor whitespace cleanup in audio_engine.cpp.

* feat(audio): implement reverb effect and integrate into underwater system

* asset(sound): update dirt and grass block break/place sounds

* feat(network): synchronize player water sound

* fix: include <numbers>

* fix(audio): play ambient water bubble sound in 3D at player position
This commit is contained in:
zhenyan121
2026-07-08 19:41:10 +08:00
committed by GitHub
parent 8a4081cf9c
commit b1341d7da4
88 changed files with 34374 additions and 53 deletions

View File

@@ -282,7 +282,7 @@ void ClientChunk::need_upload() { m_need_upload = true; }
void ClientChunk::set_chunk_block(int index, unsigned id) {
m_blocks[index] = id;
}
BlockType ClientChunk::get_chunk_block(int index) { return m_blocks[index]; }
ChunkPos ClientChunk::chunk_pos() const { return m_chunk_pos; }
BiomeType ClientChunk::biome() const { return m_biome; }

View File

@@ -1,9 +1,12 @@
#include "Cubed/gameplay/client_player.hpp"
#include "Cubed/audio/audio_engine.hpp"
#include "Cubed/config.hpp"
#include "Cubed/debug_collector.hpp"
#include "Cubed/gameplay/client_world.hpp"
namespace {} // namespace
namespace Cubed {
ClientPlayer::ClientPlayer(ClientWorld& world) : m_world(world) {}
ClientPlayer::~ClientPlayer() {}
@@ -144,42 +147,35 @@ void ClientPlayer::update_player_move_state(int key, int action) {
case GLFW_KEY_W:
if (action == GLFW_PRESS) {
m_move_state.forward = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.forward = false;
m_moving = false;
m_sprinting = false;
}
break;
case GLFW_KEY_S:
if (action == GLFW_PRESS) {
m_move_state.back = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.back = false;
m_moving = false;
}
break;
case GLFW_KEY_A:
if (action == GLFW_PRESS) {
m_move_state.left = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.left = false;
m_moving = false;
}
break;
case GLFW_KEY_D:
if (action == GLFW_PRESS) {
m_move_state.right = true;
m_moving = true;
}
if (action == GLFW_RELEASE) {
m_move_state.right = false;
m_moving = false;
}
break;
case GLFW_KEY_SPACE:
@@ -223,6 +219,8 @@ void ClientPlayer::update_player_move_state(int key, int action) {
}
break;
}
m_moving = m_move_state.forward || m_move_state.back || m_move_state.left ||
m_move_state.right;
}
void ClientPlayer::update_front_vec(float offset_x, float offset_y) {
@@ -391,6 +389,19 @@ void ClientPlayer::update_move(float delta_time) {
m_player_pos = player_pos;
}
update_player_chunk();
auto it = m_timers.find("Player Walk Sound");
if (it != m_timers.end()) {
if (m_sprinting) {
it->second.set_threshold(RUN_SOUND_INTERVAL);
} else {
it->second.set_threshold(WALK_SOUND_INTERVAL);
}
}
for (auto& [key, timer] : m_timers) {
timer.update(delta_time);
}
}
void ClientPlayer::update_x_move(glm::vec3& player_pos) {
@@ -566,7 +577,7 @@ float& ClientPlayer::fly_y_speed() { return m_fly_y_speed; }
unsigned ClientPlayer::place_block() const { return m_place_block; };
void ClientPlayer::set_gait(Gait gait) { m_gait = gait; }
GameMode& ClientPlayer::game_mode() { return m_game_mode; }
const ClientWorld& ClientPlayer::get_world() const { return m_world; }
ClientWorld& ClientPlayer::get_world() { return m_world; }
void ClientPlayer::set_uuid(std::string_view uuid) {
std::lock_guard lock(m_uuid_mutex);
@@ -578,7 +589,31 @@ std::string ClientPlayer::get_uuid() const {
return m_uuid;
}
const std::string& ClientPlayer::get_name() const { return m_name; }
void ClientPlayer::init(std::string_view name) { m_name = name; }
void ClientPlayer::init(std::string_view name) {
m_name = name;
m_timers.try_emplace("Player Walk Sound", WALK_SOUND_INTERVAL, [this]() {
if (!m_moving || is_fly) {
return;
}
glm::ivec3 block = glm::floor(m_player_pos);
block.y -= 1;
BlockType id = m_world.get_block_tpye(block);
Logger::info("player Block {} Walk Sound", id);
if (id == 0) {
return;
}
std::string name = BlockManager::name_form_id(id);
std::string sound = "block/" + name + "/walk.ogg";
auto& audio = m_world.get_audio();
audio.play_3d(sound, m_player_pos);
Logger::info("Player block {} walk sound", name);
});
}
bool ClientPlayer::is_underwater() const { return m_underwater; }
void ClientPlayer::set_underwater(bool u) { m_underwater = u; }
float ClientPlayer::yaw() const { return m_yaw; }
float ClientPlayer::pitch() const { return m_pitch; }

View File

@@ -1,6 +1,7 @@
#include "Cubed/gameplay/client_world.hpp"
#include "Cubed/config.hpp"
#include "Cubed/gameplay/chunk_generator.hpp"
#include "Cubed/gameplay/game_time.hpp"
#include "Cubed/gameplay/packet.hpp"
#include "Cubed/tools/math_tools.hpp"
@@ -20,7 +21,8 @@ struct ChunkRenderData {
};
} // namespace
ClientWorld::ClientWorld() : m_player(*this) {}
ClientWorld::ClientWorld(AudioEngine& auido)
: m_player(*this), m_audio(auido) {}
ClientWorld::~ClientWorld() {
stop_client_thread();
@@ -42,7 +44,7 @@ ClientWorld::~ClientWorld() {
}
m_pending_delete_vao.clear();
}
m_timers.clear();
m_ticktimers.clear();
}
const std::optional<LookBlock>& ClientWorld::get_look_block_pos() const {
@@ -151,24 +153,38 @@ void ClientWorld::set_block(const glm::ivec3& block_pos, unsigned id) {
auto [chunk_x, chunk_z] = get_chunk_pos(world_x, world_z);
ChunkPos pos{chunk_x, chunk_z};
BlockType origin_id = 0;
{
chunk_acc acc;
if (!m_chunks.find(acc, pos)) {
return;
}
auto [x, y, z] = ClientChunk::world_to_block(world_x, world_y, world_z,
chunk_x, chunk_z);
if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE) {
return;
}
acc->second->set_chunk_block(ClientChunk::index(x, y, z), id);
int idx = ClientChunk::index(x, y, z);
origin_id = acc->second->get_chunk_block(idx);
acc->second->set_chunk_block(idx, id);
acc->second->mark_dirty();
}
glm::vec3 sound_pos{world_x + 0.5f, world_y + 0.5f, world_z + 0.5f};
if (id == 0) {
std::string name = BlockManager::name_form_id(origin_id);
std::string sound = "block/" + name + "/break.ogg";
m_pending_sound.emplace(sound, sound_pos);
} else {
std::string name = BlockManager::name_form_id(id);
std::string sound = "block/" + name + "/place.ogg";
m_pending_sound.emplace(sound, sound_pos);
}
auto pool = m_thread_pool.load();
pool->enqueue(0, [this, pos]() {
@@ -338,12 +354,94 @@ void ClientWorld::receive_player_logout(const LogoutRsp& rsp) {
}
}
void ClientWorld::receive_player_water_sound(const PlayerWaterSound& rsp) {
if (rsp.uuid() == m_player.get_uuid()) {
return;
}
glm::vec3 pos = {rsp.pos().x(), rsp.pos().y(), rsp.pos().z()};
Logger::info("Client: Receive Player Water Sound");
m_pending_sound.emplace("ambient/water/in_and_out_of_water.flac", pos);
}
void ClientWorld::send_player_water_sound(bool underwater,
const glm::vec3& pos) {
Arena arena;
auto* r = Arena::Create<PlayerWaterSound>(&arena);
r->set_underwater(underwater);
auto* p = r->mutable_pos();
p->set_x(pos.x);
p->set_y(pos.y);
p->set_z(pos.z);
r->set_uuid(m_player.get_uuid());
m_client->send(make_packet(*r));
Logger::info("Client: Send Player Water Sound");
}
void ClientWorld::init(std::string_view player_name,
std::shared_ptr<NetworkClient> client) {
m_player.init(player_name);
m_client = client;
m_random.init(ChunkGenerator::seed());
// timer
register_timer("player_pos", 1, [this]() { report_player_info(); });
register_ticktimer("player_pos", 1, [this]() { report_player_info(); });
m_timers.try_emplace("Birds Sound", 60.0f, [this]() {
auto player_pos = m_player.get_player_pos();
if (player_pos.y < SEA_LEVEL) {
return;
}
ChunkPos pos = get_chunk_pos(player_pos.x, player_pos.z);
{
chunk_cacc cacc;
if (m_chunks.find(cacc, pos)) {
if (cacc->second->get_biome() == BiomeType::FOREST) {
m_audio.play_2d("ambient/birds.ogg", true);
}
}
}
});
m_timers.try_emplace("Ocean Wave", 3.0f, [this]() {
auto player_pos = m_player.get_player_pos();
if (player_pos.y < SEA_LEVEL - 10 || player_pos.y > SEA_LEVEL + 10) {
return;
}
auto ans = m_random.random_int(1, 4);
std::string sound =
"ambient/ocean/wave00" + std::to_string(ans) + ".flac";
ChunkPos pos = get_chunk_pos(player_pos.x, player_pos.z);
{
chunk_cacc cacc;
if (m_chunks.find(cacc, pos)) {
if (cacc->second->get_biome() == BiomeType::OCEAN) {
m_audio.play_2d(sound, true);
}
}
}
});
m_timers.try_emplace("under water bubble", 1.5f, [this]() {
if (m_player.is_underwater()) {
auto ans = m_random.random_int(1, 2);
std::string sound =
"ambient/water/bubble00" + std::to_string(ans) + ".ogg";
m_audio.play_3d(sound, m_player.get_player_pos(), true);
}
});
m_timers.try_emplace("bgm change", 350.0f, [this]() {
if (m_day_tick >= 17000 || m_day_tick < 5000) {
m_audio.change_bgm("bgm/bgm002.ogg");
} else {
m_audio.change_bgm("bgm/bgm001.mp3");
}
});
LoginReq req;
req.set_name(m_player.get_name());
while (!client->is_connected()) {
@@ -356,6 +454,7 @@ void ClientWorld::init(std::string_view player_name,
// request login
Logger::info("Send Login Request");
m_client->send(make_packet(req), 0);
m_audio.play_bgm();
}
void ClientWorld::start_client_thread(std::string_view uuid) {
@@ -427,7 +526,7 @@ void ClientWorld::client_run(std::stop_token stoken) {
auto next = Clock::now();
while (!stoken.stop_requested()) {
next += TICK;
for (auto& x : m_timers) {
for (auto& x : m_ticktimers) {
x.second.update();
}
std::this_thread::sleep_until(next);
@@ -605,6 +704,8 @@ AABB ClientWorld::get_block_aabb(const glm::ivec3& pos) {
static_cast<float>(z + 1)}};
}
AudioEngine& ClientWorld::get_audio() { return m_audio; }
void ClientWorld::request_exit() {
if (m_receive_exit) {
return;
@@ -728,6 +829,36 @@ void ClientWorld::update(float delta_time) {
player.angle = glm::mix(player.angle, 0.0f, t);
}
// walking sound
if (player.gait == Gait::STOP) {
player.moving_time = 0.0f;
} else {
player.moving_time += delta_time;
}
auto play_walk_sound = [&]() {
glm::ivec3 block = glm::floor(player.render_pos);
block.y -= 1;
BlockType id = get_block_tpye(block);
if (id == 0) {
return;
}
std::string name = BlockManager::name_form_id(id);
std::string sound = "block/" + name + "/walk.ogg";
m_audio.play_3d(sound, player.render_pos);
};
if (player.gait == Gait::WALK) {
if (player.moving_time >= ClientPlayer::WALK_SOUND_INTERVAL) {
player.moving_time = 0.0f;
play_walk_sound();
}
}
if (player.gait == Gait::RUN) {
if (player.moving_time >= ClientPlayer::RUN_SOUND_INTERVAL) {
player.moving_time = 0.0f;
play_walk_sound();
}
}
m_render_player_data.emplace_back(
player.name, player.uuid, player.render_pos, player.render_yaw,
player.render_pitch, player.gait, player.angle);
@@ -755,6 +886,16 @@ void ClientWorld::update(float delta_time) {
m_player.get_gait(), m_player.angle());
}
}
// sound
PendingSound pending_sound;
while (m_pending_sound.try_pop(pending_sound)) {
m_audio.play_3d(pending_sound.sound, pending_sound.sound_pos);
}
for (auto& [pos, timer] : m_timers) {
timer.update(delta_time);
}
}
glm::vec3 ClientWorld::sunlight_dir() const {

View File

@@ -119,6 +119,12 @@ asio::awaitable<void> NetworkClient::read_loop() {
}
}
} break;
case std::to_underlying(PacketEnum::PLAYER_WATER_SOUND): {
auto* rsp = Arena::Create<PlayerWaterSound>(&arena);
if (decode_packet(*rsp, body_data, header)) {
m_world.receive_player_water_sound(*rsp);
}
} break;
}
}
} catch (const asio::system_error& e) {

View File

@@ -597,6 +597,47 @@ void ServerWorld::sync_player_pos(const C2S_PlayerInfo& prsp) {
}
}
void ServerWorld::sync_player_water_sound(const PlayerWaterSound& rsp) {
auto x = rsp.pos().x();
auto y = rsp.pos().y();
auto z = rsp.pos().z();
ChunkPos pos = get_chunk_pos(x, z);
auto uuid = rsp.uuid();
auto underwater = rsp.underwater();
std::vector<std::shared_ptr<Session>> other;
{
std::shared_lock lock(m_player_mutex);
for (auto& [o_uuid, player] : m_players) {
if (o_uuid == uuid) {
continue;
}
if (player.has_player(pos)) {
other.emplace_back(player.get_session());
}
}
}
Arena arena;
auto* r = Arena::Create<PlayerWaterSound>(&arena);
r->set_uuid(uuid);
r->set_underwater(underwater);
auto* p = r->mutable_pos();
p->set_x(x);
p->set_y(y);
p->set_z(z);
for (auto& session : other) {
if (!session) {
continue;
}
session->send(make_packet(*r), 5);
}
}
void ServerWorld::handle_player_login(const std::string& name,
std::shared_ptr<Session> session) {
std::string uuid = generate_uuid();

View File

@@ -92,6 +92,12 @@ asio::awaitable<void> Session::read_loop() {
m_server_world.handle_player_exit(req->uuid());
}
}
if (cmd_id == std::to_underlying(PacketEnum::PLAYER_WATER_SOUND)) {
auto* req = Arena::Create<PlayerWaterSound>(&arena);
if (decode_packet(*req, body_data, header)) {
m_server_world.sync_player_water_sound(*req);
}
}
}
} catch (const asio::system_error& e) {
auto ec = e.code();