diff --git a/Makefile b/Makefile index 2267c3e..5fc26a2 100644 --- a/Makefile +++ b/Makefile @@ -117,8 +117,7 @@ $(addsuffix /Segment.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Segment.cpp $(addsuffix /Pixels.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Pixels.cpp Pixels.hpp Box.hpp extension.hpp Log.hpp math.hpp) | $(BUILD_DIRS) $(CXX) $(CXXFLAGS) $< -c -o $@ -$(addsuffix /Audio.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Audio.cpp Audio.hpp Node.hpp Display.hpp Configuration.hpp Box.hpp filesystem.hpp extension.hpp \ - Animation.hpp) | $(BUILD_DIRS) +$(addsuffix /Audio.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Audio.cpp Audio.hpp Node.hpp filesystem.hpp) | $(BUILD_DIRS) $(CXX) $(CXXFLAGS) $< -c -o $@ $(addsuffix /GLObject.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, GLObject.cpp GLObject.hpp Log.hpp) | $(BUILD_DIRS) @@ -136,7 +135,7 @@ $(addsuffix /Attributes.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Attribute $(addsuffix /Model.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Model.cpp Model.hpp extension.hpp Attributes.hpp Texture.hpp Carousel.hpp) | $(BUILD_DIRS) $(CXX) $(CXXFLAGS) $< -c -o $@ -$(addsuffix /Text.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Text.cpp Text.hpp Model.hpp Color.hpp) | $(BUILD_DIRS) +$(addsuffix /Text.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Text.cpp Text.hpp Model.hpp Color.hpp Log.hpp) | $(BUILD_DIRS) $(CXX) $(CXXFLAGS) $< -c -o $@ $(addsuffix /Color.o, $(BUILD_DIRS)): $(addprefix $(SB_SRC_DIR)/, Color.cpp Color.hpp) | $(BUILD_DIRS) @@ -199,7 +198,9 @@ EMSCRIPTENHOME = $(HOME)/ext/software/emsdk/upstream/emscripten EMSCRIPTEN_CFLAGS = -O0 -Wall -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS="['png', 'jpg']" -s USE_SDL_TTF=2 -s USE_SDL_MIXER=2 \ --no-heap-copy -I $(SB_LIB_DIR) -I $(SB_SRC_DIR) EMSCRIPTEN_LFLAGS = -s MIN_WEBGL_VERSION=2 -s EXPORTED_FUNCTIONS="['_main', '_malloc']" \ - -s LLD_REPORT_UNDEFINED -s NO_DISABLE_EXCEPTION_CATCHING -s FULL_ES3=1 -s ALLOW_MEMORY_GROWTH=1 -lidbfs.js + -s LLD_REPORT_UNDEFINED -s NO_DISABLE_EXCEPTION_CATCHING -s FULL_ES3=1 -lidbfs.js \ + -s TOTAL_MEMORY=200MB -s ALLOW_MEMORY_GROWTH=0 + # -s ALLOW_MEMORY_GROWTH=1 EMSCRIPTEN_PRELOADS = --preload-file "BPmono.ttf"@/ --preload-file "config.json"@/ --preload-file "config_wasm.json"@/ --preload-file "resource/"@/"resource/" \ --preload-file "src/shaders/"@/"src/shaders/" EMSCRIPTEN_GAME_CONFIGS = config.json config_wasm.json resource/levels.json @@ -219,7 +220,8 @@ cakefoot_debug.html : CFLAGS = $(EMSCRIPTEN_CFLAGS) -g2 cakefoot_debug.html : CXXFLAGS = $(CFLAGS) --std=c++17 cakefoot_debug.html : $(WASM_OBJS) $(EMSCRIPTEN_GAME_CONFIGS) $(CREATE_FONT_SYMLINK) - $(CXX) $(filter-out $(EMSCRIPTEN_GAME_CONFIGS), $^) $(EMSCRIPTEN_LFLAGS) $(EMSCRIPTEN_PRELOADS) --memoryprofiler --cpuprofiler -o cakefoot_debug.html + $(CXX) $(filter-out $(EMSCRIPTEN_GAME_CONFIGS), $^) $(CXXFLAGS) $(EMSCRIPTEN_LFLAGS) $(EMSCRIPTEN_PRELOADS) --memoryprofiler --cpuprofiler \ + -o cakefoot_debug.html ################# # Android build # @@ -284,7 +286,7 @@ $(ANDROID_BUILD_DIR)/app-debug.apk: $(ANDROID_BUILD_DIR) $(ANDROID_BUILD_DIR)/$( # Make all builds # ################### -all : Cakefoot-linux_debug.x86_64 cakefoot.js +all : Cakefoot-linux_debug.x86_64 cakefoot.js cakefoot_debug.html ######################### # Clean up object files # diff --git a/config.json b/config.json index 8a6cced..3dd0983 100644 --- a/config.json +++ b/config.json @@ -113,11 +113,11 @@ }, { "name": "BUFFALO BEEF CAKE", - "speed increment": 0.005, + "speed increment": 0.01, "speed decrement": 0.0455, - "max speed": 0.82, + "max speed": 0.95, "min speed": -0.3125, - "increment mod": 0.025, + "increment mod": 0.08, "decrement mod": 0.125, "animation frames": ["resource/buffalo/cake1.png"] } @@ -156,7 +156,11 @@ "profile decrement text": "<", "profile decrement translation": [-0.65, -0.83], "profile increment text": ">", - "profile increment translation": [0.65, -0.83] + "profile increment translation": [0.65, -0.83], + "volume on texture": "resource/vol.png", + "volume off texture": "resource/vol_off.png", + "volume translation": [-1.65, -0.85], + "volume scale": 0.08 }, "world": [ @@ -176,5 +180,30 @@ "start": 18, "color": [0.23, 0.12, 0.18, 1.0] } - ] + ], + + "audio": { + "files": + { + "restart": "resource/no.ogg", + "teleport": "resource/grow_0.wav", + "walk": "resource/bump_4.wav", + "reverse": "resource/bump_5.wav", + "main": "resource/azu main theme_amp.ogg", + "menu": "resource/azu menu music_amp.ogg", + "take": "resource/Coin_.wav", + "checkpoint": "resource/arrive_0.wav" + }, + "volume": { + "restart": 0.5, + "teleport": 0.7, + "walk": 0.5, + "reverse": 0.46, + "main": 1.0, + "menu": 1.0, + "take": 0.5, + "checkpoint": 0.5 + }, + "fade": 2.0 + } } diff --git a/lib/sb b/lib/sb index 5046b4b..d575307 160000 --- a/lib/sb +++ b/lib/sb @@ -1 +1 @@ -Subproject commit 5046b4bcf1696296725ad4ab2bab7f589e97d7c8 +Subproject commit d575307b15024bcf7259f06126fd8d61adb455ac diff --git a/resource/Coin_.wav b/resource/Coin_.wav new file mode 100644 index 0000000..32b2965 Binary files /dev/null and b/resource/Coin_.wav differ diff --git a/resource/arrive_0.wav b/resource/arrive_0.wav new file mode 100644 index 0000000..1bb0033 Binary files /dev/null and b/resource/arrive_0.wav differ diff --git a/resource/azu main theme_amp.ogg b/resource/azu main theme_amp.ogg new file mode 100644 index 0000000..dbef2b5 Binary files /dev/null and b/resource/azu main theme_amp.ogg differ diff --git a/resource/azu menu music_amp.ogg b/resource/azu menu music_amp.ogg new file mode 100644 index 0000000..dadb073 Binary files /dev/null and b/resource/azu menu music_amp.ogg differ diff --git a/resource/bump_4.wav b/resource/bump_4.wav new file mode 100644 index 0000000..96da733 Binary files /dev/null and b/resource/bump_4.wav differ diff --git a/resource/bump_5.wav b/resource/bump_5.wav new file mode 100644 index 0000000..7543efd Binary files /dev/null and b/resource/bump_5.wav differ diff --git a/resource/grow_0.wav b/resource/grow_0.wav new file mode 100644 index 0000000..e1e590e Binary files /dev/null and b/resource/grow_0.wav differ diff --git a/resource/no.ogg b/resource/no.ogg new file mode 100644 index 0000000..c56df93 Binary files /dev/null and b/resource/no.ogg differ diff --git a/resource/pause.png b/resource/pause.png index d62983d..5fc6dfe 100644 Binary files a/resource/pause.png and b/resource/pause.png differ diff --git a/resource/reverse.ogg b/resource/reverse.ogg new file mode 100644 index 0000000..b4429e8 Binary files /dev/null and b/resource/reverse.ogg differ diff --git a/resource/vol.png b/resource/vol.png new file mode 100644 index 0000000..d4b8f20 Binary files /dev/null and b/resource/vol.png differ diff --git a/resource/vol_off.png b/resource/vol_off.png new file mode 100644 index 0000000..0498e6a Binary files /dev/null and b/resource/vol_off.png differ diff --git a/src/Cakefoot.cpp b/src/Cakefoot.cpp index 3e2743b..661a923 100644 --- a/src/Cakefoot.cpp +++ b/src/Cakefoot.cpp @@ -97,9 +97,44 @@ Cakefoot::Cakefoot() /* Load coin graphics */ coin.load(); + /* Load SFX and BGM */ + load_audio(); + /* Load title screen and character graphics */ character.load(); load_level(0); + + /* Switch volume on */ + button.at("volume").press(); +} + +void Cakefoot::load_audio() +{ + audio = {}; + for (const auto& [name, path] : configuration()("audio", "files").items()) + { + audio[name] = sb::audio::Chunk(path.get()); + } + + /* Set number of loops for looping audio */ + audio.at("restart").loop(5); + audio.at("main").loop(); + audio.at("menu").loop(); + audio.at("walk").loop(); + audio.at("reverse").loop(); + + /* Set volumes */ + for (const auto& [name, volume] : configuration()("audio", "volume").items()) + { + audio.at(name).volume(volume); + } + + /* Reserve two channels for walk sound effects so the channel volume manipulation won't affect other sounds. */ + int result = Mix_ReserveChannels(2); + if (result != 2) + { + sb::Log::sdl_error("Unable to reserve audio channels in SDL mixer", sb::Log::WARN); + } } void Cakefoot::load_curves() @@ -234,8 +269,23 @@ void Cakefoot::set_up_buttons() button.at("resume").on_state_change([&](bool state){ unpaused_timer.on(); run_timer.on(); + + /* Transition between menu theme and main theme */ + if (audio.at("menu").playing()) + { + audio.at("menu").pause(); + } + if (audio.at("main").paused()) + { + audio.at("main").resume(); + } + else if (audio.at("main").fading() || !audio.at("main").playing()) + { + audio.at("main").play(); + } }); button.at("reset").on_state_change([&](bool state){ + audio.at("main").stop(); sb::Delegate::post(reset_command_name, false); }); @@ -250,6 +300,65 @@ void Cakefoot::set_up_buttons() button.at("pause").on_state_change([&](bool state){ unpaused_timer.off(); run_timer.off(); + + std::cout << std::boolalpha << "menu playing: " << audio.at("menu").playing() << ", menu fading: " << audio.at("menu").fading() << std::endl; + + /* Transition between main theme and menu theme */ + if (audio.at("main").playing()) + { + audio.at("main").pause(); + } + if (audio.at("menu").paused()) + { + audio.at("menu").resume(); + } + else if (audio.at("menu").fading() || !audio.at("menu").playing()) + { + audio.at("menu").play(); + } + + std::cout << std::boolalpha << "menu playing: " << audio.at("menu").playing() << ", menu fading: " << audio.at("menu").fading() << std::endl; + + }); + + /* Set up volume button */ + bool original_state = button.at("volume").pressed(); + sb::Texture volume_off_texture {configuration()("button", "volume off texture")}; + volume_off_texture.load(); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + sb::Texture volume_on_texture {configuration()("button", "volume on texture")}; + volume_on_texture.load(); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + sb::Plane volume_plane; + volume_plane.texture(volume_off_texture); + volume_plane.texture(volume_on_texture); + button.at("volume") = sb::Pad<>{volume_plane, configuration()("button", "volume translation"), configuration()("button", "volume scale"), 1.0f}; + button.at("volume").state(original_state); + button.at("volume").on_state_change([&](bool state){ + /* BGM is paused to get around a bug in looping that causes looped audio to stop when looping on mute. */ + if (state) + { + Mix_Volume(-1, 128); + // for (auto& [name, chunk] : audio) + // { + // chunk.channel_volume(MIX_MAX_VOLUME); + // } + } + else + { + for (auto& [name, chunk] : audio) + { + if (chunk.fading()) + { + chunk.stop(); + chunk.play(); + } + // chunk.channel_volume(0); + } + Mix_Volume(-1, 0); + } }); /* Set up level select spinner */ @@ -417,6 +526,42 @@ void Cakefoot::load_level(int index) /* Wrap the index if it is out of range. */ index = glm::mod(index, configuration()("levels").size()); + /* Play menu theme on title screen and end screen. Play main theme on any other level. Cross fade between the two. */ + // float fade = configuration()("audio", "fade"); + if (index == 0 || static_cast(index) == configuration()("levels").size() - 1) + { + /* If menu theme is already playing, let it continue to play. */ + // audio.at("menu").stop(); + if (!audio.at("menu").playing() || audio.at("menu").paused() || audio.at("menu").fading()) + { + // audio.at("menu").play(fade); + audio.at("menu").play(); + } + + /* Cross fade main theme into menu theme */ + // if (audio.at("main").playing()) + // { + // audio.at("main").stop(fade); + audio.at("main").stop(); + // } + } + else + { + // audio.at("main").stop(); + if (audio.at("main").paused() || !audio.at("main").playing()) + { + // audio.at("main").play(fade); + audio.at("main").play(); + } + + /* If the menu theme is playing, cross fade to the main theme. */ + // if (audio.at("menu").playing()) + // { + // audio.at("menu").stop(fade); + audio.at("menu").stop(); + // } + } + /* Update indices and reset character. */ level_index = index; curve_index = index; @@ -770,6 +915,20 @@ void Cakefoot::respond(SDL_Event& event) } } + /* Collide with volume button */ + else if (button.at("volume").collide(mouse_ndc, view, projection)) + { + if (event.type == SDL_MOUSEBUTTONDOWN) + { + button.at("volume").press(); + button_pressed = true; + } + else + { + hovering = true; + } + } + /* Check pause menu buttons */ else if (level_index > 0 && !unpaused_timer) { @@ -846,6 +1005,7 @@ void Cakefoot::respond(SDL_Event& event) character.box_size(configuration()("character", "hitbox").get()); set_up_buttons(); set_up_hud(); + load_audio(); } else if (sb::Delegate::compare(event, "window resize")) @@ -936,9 +1096,10 @@ void Cakefoot::update(float timestamp) /* Update character, along the curve, using the timer to determine movement since last frame, and update enemies. Check for collison * as enemies are updated. */ - character.update(curve(), unpaused_timer); + character.update(curve(), unpaused_timer, !button.at("volume").pressed()); if (character.at_end(curve())) { + audio.at("teleport").play(); load_level(level_index + 1); } else @@ -950,6 +1111,7 @@ void Cakefoot::update(float timestamp) { if (character.relative(curve()) >= checkpoint["position"].get() && character.checkpoint() < checkpoint["position"].get()) { + audio.at("checkpoint").play(); character.checkpoint(checkpoint["position"].get()); /* Collect any previously taken coins */ @@ -976,13 +1138,15 @@ void Cakefoot::update(float timestamp) } else if (enemy->collide_coin(character.box(), clip_upper, clip_lower)) { + audio.at("take").play(); enemy->take_coin(); } } /* Reset level */ - if (enemy_collision) + if (!character.resting() && enemy_collision) { + audio.at("restart").play(); character.spawn(curve()); for (auto& enemy : enemies) { @@ -1072,6 +1236,7 @@ void Cakefoot::update(float timestamp) character.draw(curve(), uniform["mvp"], view * rotation_matrix, projection, uniform["texture enabled"]); /* Draw buttons. Don't include rotation matrix in view, so buttons will remain flat in the z-dimension. */ + button.at("volume").draw(uniform["mvp"], view, projection, uniform["texture enabled"]); if (level_index == 0) { button.at("start").draw(uniform["mvp"], view, projection, uniform["texture enabled"]); @@ -1085,7 +1250,7 @@ void Cakefoot::update(float timestamp) /* Draw spinner labels */ for (const std::string& name : {"level select", "profile"}) { - label.at(name).texture().bind(); + label.at(name).texture(0).bind(); glm::mat4 label_transformation = projection * view * label.at(name).transformation(); glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]); label.at(name).enable(); @@ -1115,7 +1280,7 @@ void Cakefoot::update(float timestamp) label.at("clock").content(clock.str()); sb::Plane::position->bind("vertex_position", shader_program); glUniform1i(uniform["texture enabled"], true); - label.at("clock").texture().bind(); + label.at("clock").texture(0).bind(); glm::mat4 clock_transformation = projection * view * label.at("clock").transformation(); glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &clock_transformation[0][0]); label.at("clock").enable(); @@ -1127,7 +1292,7 @@ void Cakefoot::update(float timestamp) std::stringstream level_indicator; level_indicator << std::setw(2) << std::setfill('0') << level_index << "/" << std::setw(2) << _configuration("levels").size() - 2; label.at("level").content(level_indicator.str()); - label.at("level").texture().bind(); + label.at("level").texture(0).bind(); glm::mat4 level_transformation = projection * view * label.at("level").transformation(); glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &level_transformation[0][0]); label.at("level").enable(); @@ -1143,12 +1308,12 @@ void Cakefoot::update(float timestamp) label.at("fps").content(padded); previous_frames_per_second = current_frames_per_second; } - if (label.at("fps").texture().generated()) + if (label.at("fps").texture(0).generated()) { /* Draw FPS indicator */ sb::Plane::position->bind("vertex_position", shader_program); glUniform1i(uniform["texture enabled"], true); - label.at("fps").texture().bind(); + label.at("fps").texture(0).bind(); glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label.at("fps").transformation()[0][0]); label.at("fps").enable(); glDrawArrays(GL_TRIANGLES, 0, label.at("fps").attributes("position")->count()); diff --git a/src/Cakefoot.hpp b/src/Cakefoot.hpp index f48b738..8cd0c89 100644 --- a/src/Cakefoot.hpp +++ b/src/Cakefoot.hpp @@ -94,7 +94,8 @@ private: {"level decrement", sb::Pad<>()}, {"pause", sb::Pad<>()}, {"profile increment", sb::Pad<>()}, - {"profile decrement", sb::Pad<>()} + {"profile decrement", sb::Pad<>()}, + {"volume", sb::Pad<>()} }; std::map label = { {"fps", sb::Text(font())}, @@ -105,7 +106,6 @@ private: }; sb::Sprite playing_field, checkpoint_on, checkpoint_off, coin {"resource/coin/coin-0.png", glm::vec2{12.0f / 486.0f}}; sb::Timer on_timer, run_timer, unpaused_timer; - Character character {_configuration}; glm::vec3 camera_position {0.0f, 0.0f, 2.0f}, subject_position {0.0f, 0.0f, 0.0f}; float zoom = 0.0f; glm::vec2 rotation = {0.0f, 0.0f}; @@ -113,6 +113,14 @@ private: std::vector> enemies; glm::vec4 world_color {0.2f, 0.2f, 0.2f, 1.0f}; std::map> fonts; + std::map audio; + Character character {_configuration, audio}; + + /*! + * Load sound effects and music into objects that can be used by the SDL mixer library. Use chunk objects for background music instead of + * music objects so background music tracks can fade into each other. + */ + void load_audio(); /*! * Open configuration and load curve data into the object. diff --git a/src/Character.cpp b/src/Character.cpp index 200c3d5..f7a4591 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -1,6 +1,7 @@ #include "Character.hpp" -Character::Character(const Configuration& configuration) : configuration(configuration) {} +Character::Character(const Configuration& configuration, std::map& audio) : + configuration(configuration), audio(audio) {} void Character::profile(const std::string& name) { @@ -61,6 +62,9 @@ void Character::spawn(const Curve& curve) position = curve[next_point_index]; speed = 0.0f; accelerating = false; + _resting = true; + audio.at("walk").stop(); + audio.at("reverse").stop(); } void Character::checkpoint(float checkpoint) @@ -78,68 +82,126 @@ bool Character::at_end(const Curve& curve) const return next_point_index > curve.length() - 1; } +bool Character::resting() const +{ + return _resting; +} + float Character::relative(const Curve& curve) const { return float(next_point_index) / curve.length(); } -void Character::update(const Curve& curve, const sb::Timer& timer) +void Character::update(const Curve& curve, const sb::Timer& timer, bool muted) { - /* Adjust speed based on acceleration state and character profile. */ - if (accelerating) + if (timer.frame() > 0.0f) { - /* Apply delta time to the speed increase. */ - speed += timer.delta(profile()["speed increment"].get()) + glm::abs(speed) * profile()["increment mod"].get(); - } - else - { - /* Apply delta time to the speed decrease. */ - speed -= timer.delta(profile()["speed decrement"].get()) + glm::abs(speed) * profile()["decrement mod"].get(); - } + /* Adjust speed based on acceleration state and character profile. */ + if (accelerating) + { + _resting = false; - /* Clamp speed, applying delta time to the limits */ - speed = std::clamp(speed, timer.delta(profile()["min speed"].get()), timer.delta(profile()["max speed"].get())); - - /* Move along unwrapped curve vertices */ - float distance_remaining = std::abs(speed), distance = 0.0f; - glm::vec3 next_point, step; - while (distance_remaining) - { - if (speed < 0.0f && relative(curve) <= checkpoint()) - { - speed = 0.0f; - break; - } - else if (speed > 0.0f && next_point_index > curve.length() - 1) - { - speed = 0.0f; - break; - } - if (speed > 0.0f) - { - next_point = curve[next_point_index]; + /* Apply delta time to the speed increase. */ + speed += timer.delta(profile()["speed increment"].get()) + glm::abs(speed) * profile()["increment mod"].get(); } else { - next_point = curve[next_point_index - 1]; + /* Apply delta time to the speed decrease. */ + speed -= timer.delta(profile()["speed decrement"].get()) + glm::abs(speed) * profile()["decrement mod"].get(); } - distance = glm::distance(position, next_point); - if (distance < distance_remaining) + + /* Clamp speed, applying delta time to the limits */ + float max_speed = timer.delta(profile()["max speed"].get()); + float min_speed = timer.delta(profile()["min speed"].get()); + speed = std::clamp(speed, min_speed, max_speed); + + /* Calculate volume based on speed relative to max speed */ + int volume; + if (speed >= 0.0f) { - distance_remaining -= distance; - position = next_point; - next_point_index += speed < 0.0f ? -1 : 1; + /* Only play walking forward effect. */ + audio.at("reverse").stop(); + if (!audio.at("walk").playing()) + { + audio.at("walk").play(0.0f, walk_channel); + } + + if (!muted) + { + /* Get louder closer to max speed using an exponential scale */ + volume = std::round(std::pow(speed / max_speed, 3.0f) * static_cast(MIX_MAX_VOLUME)); + audio.at("walk").channel_volume(volume); + } + else + { + audio.at("walk").stop(); + } } else { - step = glm::vec3{sb::Segment(position, next_point).step(distance_remaining), 0.0f}; - position += step; - distance_remaining = 0; - } - } + /* Only play walking backward effect. */ + audio.at("walk").stop(); + if (!audio.at("reverse").playing()) + { + audio.at("reverse").play(0.0f, reverse_channel); + } - /* Update collision box location. */ - _box.south(position); + if (!muted) + { + /* Get louder closer to min speed using an exponential scale */ + volume = std::round(std::pow(speed / min_speed, 3.0f) * static_cast(MIX_MAX_VOLUME)); + audio.at("reverse").channel_volume(volume); + } + else + { + audio.at("reverse").stop(); + } + } + + /* Move along unwrapped curve vertices */ + float distance_remaining = std::abs(speed), distance = 0.0f; + glm::vec3 next_point, step; + while (distance_remaining) + { + if (speed < 0.0f && (relative(curve) <= 0.0f || (resting() && relative(curve) <= checkpoint()))) + { + _resting = true; + audio.at("walk").stop(); + audio.at("reverse").stop(); + speed = 0.0f; + break; + } + else if (speed > 0.0f && next_point_index > curve.length() - 1) + { + speed = 0.0f; + break; + } + if (speed > 0.0f) + { + next_point = curve[next_point_index]; + } + else + { + next_point = curve[next_point_index - 1]; + } + distance = glm::distance(position, next_point); + if (distance < distance_remaining) + { + distance_remaining -= distance; + position = next_point; + next_point_index += speed < 0.0f ? -1 : 1; + } + else + { + step = glm::vec3{sb::Segment(position, next_point).step(distance_remaining), 0.0f}; + position += step; + distance_remaining = 0; + } + } + + /* Update collision box location. */ + _box.south(position); + } } void Character::draw(const Curve& curve, GLuint transformation_uniform, const glm::mat4 view, const glm::mat4 projection, GLuint texture_flag_uniform) diff --git a/src/Character.hpp b/src/Character.hpp index b6cbfd4..c7e1581 100644 --- a/src/Character.hpp +++ b/src/Character.hpp @@ -2,8 +2,10 @@ #include #include +#include #include "json/json.hpp" +#include "SDL_mixer.h" #include "Configuration.hpp" #include "Switch.hpp" @@ -11,6 +13,7 @@ #include "Sprite.hpp" #include "Curve.hpp" #include "Segment.hpp" +#include "Audio.hpp" class Character { @@ -20,8 +23,9 @@ private: /* Convert pixel size to NDC size. */ inline static const glm::vec2 size {20.0f / 486.0f}; - /* Keep a reference to the global configuration */ + /* Keep references to the global configuration and audio data */ const Configuration& configuration; + std::map& audio; /* Set at runtime based on the configuration */ sb::Sprite _sprite; @@ -31,6 +35,7 @@ private: float speed {0.0f}, _checkpoint {0.0f}; int next_point_index {0}; Box _box {{0.0f, 0.0f}, 2.0f * this->size}; + bool _resting = true; /*! * A JSON object containing the fields: name, speed increment, speed decrement, max speed, increment mod, decrement mod, and animation frames. @@ -43,6 +48,10 @@ private: public: + /* Two SDL mixer channels reserved for walk sound effects */ + inline static const int walk_channel = 0; + inline static const int reverse_channel = 1; + /* Create an object which can be switched on and off to move and stop the character. */ sb::Switch<> accelerating = false; @@ -52,7 +61,7 @@ public: /*! * @param configuration reference to the global configuration which is expected to contain profile entries for character physics */ - Character(const Configuration& configuration); + Character(const Configuration& configuration, std::map& audio); /*! * Change the character's physics and texture by specifying a profile name. The name refers to a JSON profile stored in an array of profiles @@ -121,6 +130,11 @@ public: */ bool at_end(const Curve& curve) const; + /*! + * @return True if the character has just respawned or the level just began + */ + bool resting() const; + /*! * @return character's relative position on the given curve */ @@ -131,8 +145,9 @@ public: * * @param curve the curve to update against * @param timer a timer object that is updated once per frame, so that it provides delta time for movement + * @param muted flag for preventing the walk sound effect output */ - void update(const Curve& curve, const sb::Timer& timer); + void update(const Curve& curve, const sb::Timer& timer, bool muted = false); /*! * Perform GL drawing operations using the character's sprite object. diff --git a/www/collect.php b/www/collect.php index 733dbef..acc807f 100644 --- a/www/collect.php +++ b/www/collect.php @@ -1,25 +1,35 @@ json_decode(file_get_contents("php://input"), true)["progress"]); +/* Merge the passed play history into the history array, overwriting any existing data at the current session ID, or adding a new section + * to the array if the session ID doesn't exist as a key in the array yet. Write the array to the history path. */ file_put_contents( $history_path, json_encode( array_merge($history, $submitted_user_log), JSON_PRETTY_PRINT) . "\n"); +/* Print the session ID formatted as JSON, so that the JavaScript program can get the ID as a response. */ echo json_encode(array("id" => session_id())); ?>