- add sound effects and background music

- add mute button
- add walk and reverse adaptive sound effects to character
- add more flexible error handling to PHP log of play time
- set total memory to 200MB in web build
This commit is contained in:
ohsqueezy 2023-10-09 23:54:47 -04:00
parent a090c52178
commit ac095283f4
20 changed files with 364 additions and 73 deletions

View File

@ -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 #

View File

@ -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
}
}

2
lib/sb

@ -1 +1 @@
Subproject commit 5046b4bcf1696296725ad4ab2bab7f589e97d7c8
Subproject commit d575307b15024bcf7259f06126fd8d61adb455ac

BIN
resource/Coin_.wav Normal file

Binary file not shown.

BIN
resource/arrive_0.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resource/bump_4.wav Normal file

Binary file not shown.

BIN
resource/bump_5.wav Normal file

Binary file not shown.

BIN
resource/grow_0.wav Normal file

Binary file not shown.

BIN
resource/no.ogg Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
resource/reverse.ogg Normal file

Binary file not shown.

BIN
resource/vol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
resource/vol_off.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -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<fs::path>());
}
/* 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<std::size_t>(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<float>());
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<float>() && character.checkpoint() < checkpoint["position"].get<float>())
{
audio.at("checkpoint").play();
character.checkpoint(checkpoint["position"].get<float>());
/* 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());

View File

@ -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<std::string, sb::Text> 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<std::shared_ptr<Enemy>> enemies;
glm::vec4 world_color {0.2f, 0.2f, 0.2f, 1.0f};
std::map<std::string, std::shared_ptr<TTF_Font>> fonts;
std::map<std::string, sb::audio::Chunk> 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.

View File

@ -1,6 +1,7 @@
#include "Character.hpp"
Character::Character(const Configuration& configuration) : configuration(configuration) {}
Character::Character(const Configuration& configuration, std::map<std::string, sb::audio::Chunk>& 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<float>()) + glm::abs(speed) * profile()["increment mod"].get<float>();
}
else
{
/* Apply delta time to the speed decrease. */
speed -= timer.delta(profile()["speed decrement"].get<float>()) + glm::abs(speed) * profile()["decrement mod"].get<float>();
}
/* 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<float>()), timer.delta(profile()["max speed"].get<float>()));
/* 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<float>()) + glm::abs(speed) * profile()["increment mod"].get<float>();
}
else
{
next_point = curve[next_point_index - 1];
/* Apply delta time to the speed decrease. */
speed -= timer.delta(profile()["speed decrement"].get<float>()) + glm::abs(speed) * profile()["decrement mod"].get<float>();
}
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>());
float min_speed = timer.delta(profile()["min speed"].get<float>());
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<float>(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<float>(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)

View File

@ -2,8 +2,10 @@
#include <vector>
#include <algorithm>
#include <map>
#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<std::string, sb::audio::Chunk>& 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<std::string, sb::audio::Chunk>& 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.

View File

@ -1,25 +1,35 @@
<?php
/* Get a unique ID from the browser which PHP creates and manages using a browser cookie */
session_start();
/* The log of play history is stored in JSON format in a file in the same directory as this script. If this file doesn't exist yet, it
* will be created at write time. */
$history_path = "Play_History.json";
if (file_exists($history_path))
/* Read JSON data from the history path. If the file doesn't exist, is not in JSON format, or any other exception occurs, just set the
* history to an empty array. */
try
{
$history = json_decode(file_get_contents($history_path), true);
$history = json_decode(file_get_contents($history_path), true, 512, JSON_THROW_ON_ERROR);
}
else
catch (Exception $e)
{
$history = array();
}
/* JSON data containing the user's play history is passed as a POST request from JavaScript */
$submitted_user_log = array(session_id() => 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()));
?>