separate classes into files

This commit is contained in:
Cocktail Frank 2023-06-19 22:59:11 -04:00
parent 46d0eaec9e
commit a71455fbd5
9 changed files with 651 additions and 617 deletions

View File

@ -42,7 +42,7 @@ SB_O_FILES := $(patsubst %.cpp, %.o, $(wildcard $(addprefix $(SB_SRC_DIR),*.cpp)
# Get all project header and object files
SRC_H_FILES := $(wildcard $(addprefix $(SRC_DIR),*.hpp))
SRC_O_FILES := $(SRC_H_FILES:.hpp=.o)
SRC_O_FILES := $(patsubst %.cpp, %.o, $(wildcard $(addprefix $(SRC_DIR),*.cpp)))
##################################################################
# Object files for [SPACEBOX], its dependencies, and the project #

View File

@ -81,8 +81,8 @@ Cakefoot::Cakefoot()
poke = std::shared_ptr<SDL_Cursor>(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND), SDL_FreeCursor);
grab = std::shared_ptr<SDL_Cursor>(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEALL), SDL_FreeCursor);
character_sprite = Sprite("resource/cake-frames/cake1.png", glm::vec2{20.0f / 486.0f});
/* Initialize character sprite and position */
character.sprite = Sprite("resource/cake-frames/cake1.png", glm::vec2{20.0f / 486.0f});
character.reset(curve());
}
@ -147,11 +147,14 @@ void Cakefoot::load_gl_context()
/* Set the active texture and uniform once at context load time because only one texture is used per draw */
glActiveTexture(GL_TEXTURE0);
glUniform1i(uniform["model texture"], 0);
sb::Log::gl_errors("after uniform locations");
/* Enable alpha rendering */
/* Enable alpha rendering, disable depth test, set clear color */
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
sb::Log::gl_errors("after uniform locations");
glDisable(GL_DEPTH_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
sb::Log::gl_errors("after GL initialization");
}
Curve& Cakefoot::curve()
@ -320,22 +323,18 @@ void Cakefoot::update(float timestamp)
{
sb::Log::gl_errors("at beginning of update");
/* Time in seconds the game has been running for */
/* Update time in seconds the game has been running for, pass to the shader. */
if (!timer)
{
timer.on();
}
timer.update(timestamp);
set_framerate(configuration()["display"]["framerate"]);
character.update(curve(), timer);
glDisable(GL_DEPTH_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUniform1f(uniform["time"], timer.elapsed());
/* Update character, along the curve, using the timer to determine movement since last frame. */
character.update(curve(), timer);
/* Transformation for rotating the model space and looking at the center of the field of play from the camera position. */
view = glm::lookAt(camera_position, {0.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}) *
glm::rotate(glm::mat4(1), rotation.x, {0.0f, 1.0f, 0.0f}) * glm::rotate(glm::mat4(1), rotation.y, {1.0f, 0.0f, 0.0f});
@ -349,6 +348,9 @@ void Cakefoot::update(float timestamp)
/* Plane position vertices will be used for everything before the curve */
sb::Plane::position->bind("vertex_position", shader_program);
/* Clear screen to black */
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* Draw playing field */
field.attributes("color")->bind("vertex_color", shader_program);
// field.draw(uniform["mvp"], uniform["texture enabled"], view, projection);
@ -376,9 +378,9 @@ void Cakefoot::update(float timestamp)
/* Draw cake */
sb::Plane::position->bind("vertex_position", shader_program);
character_sprite.translate(glm::vec3{cake_translation, 0.0f});
character_sprite.bind_texture(uniform["texture enabled"]);
character_sprite.draw(uniform["mvp"], view, projection);
character.sprite.translate(glm::vec3{cake_translation, 0.0f});
character.sprite.bind_texture(uniform["texture enabled"]);
character.sprite.draw(uniform["mvp"], view, projection);
/* Update FPS indicator display to the current FPS count and draw. */
if (configuration()["display"]["fps"])
@ -406,97 +408,6 @@ void Cakefoot::update(float timestamp)
sb::Log::gl_errors("at end of update");
}
int Curve::length() const
{
return unwrapped.size();
}
const glm::vec2& Curve::front() const
{
return unwrapped.front();
}
const glm::vec2& Curve::operator[](int index) const
{
return unwrapped[index];
}
void Character::reset(const Curve& curve)
{
next_point_index = 0;
speed = 0.0f;
position = curve.front();
accelerating = false;
}
const nlohmann::json& Character::entry(const std::string& suffix) const
{
return configuration["character"][*profile.current() + "-" + suffix];
}
void Character::update(const Curve& curve, const sb::Timer& timer)
{
/* Adjust speed based on acceleration state and character profile. */
if (accelerating)
{
/* Apply delta time to the speed increase. */
speed += timer.delta(entry("speed-increment").get<float>()) + glm::abs(speed) * entry("increment-mod").get<float>();
}
else
{
/* Apply delta time to the speed decrease. */
speed -= timer.delta(entry("speed-decrement").get<float>()) + glm::abs(speed) * entry("decrement-mod").get<float>();
}
/* Clamp speed, applying delta time to the limits */
if (speed > timer.delta(entry("max-speed").get<float>()))
{
speed = timer.delta(entry("max-speed").get<float>());
}
else if (speed < timer.delta(entry("min-speed").get<float>()))
{
speed = timer.delta(entry("min-speed").get<float>());
}
/* Move along unwrapped curve vertices */
float distance_remaining = std::abs(speed), distance = 0.0f;
glm::vec2 next_point, step;
while (distance_remaining)
{
if (speed < 0.0f && next_point_index == 0)
{
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 = sb::Segment(position, next_point).step(distance_remaining);
position += step;
distance_remaining = 0;
}
}
}
glm::vec4 world_to_clip(glm::vec3 world, glm::mat4 projection, glm::mat4 view)
{
return projection * (view * glm::vec4{world.x, world.y, world.z, 1.0f});

View File

@ -43,515 +43,11 @@
#include "Timer.hpp"
#include "Text.hpp"
class Sprite : private sb::Plane
{
private:
/* Keep a reference of the matrix transformations generated when the user applys a transformation, so that each can be reapplied
* without having to set all the transformations every time one is changed. */
glm::mat4 _scale {1.0f}, _translation {1.0f}, _rotation {1.0f};
public:
/*!
* Construct a Sprite with an optional scale amount.
*
* @param scale amount to scale
*/
Sprite(glm::vec2 scale = glm::vec2{1.0f})
{
this->scale(scale);
}
/*!
* Construct a Sprite from a texture. The texture is the 2D graphic that displays in the Sprite's location.
*
* @param texture sprite's 2D graphic
* @param scale amount to scale
*/
Sprite(const sb::Texture& texture, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
this->texture(texture);
}
/*!
* Construct a Sprite from a list of textures. Each texture is a frame of the Sprite's animation.
*
* @param textures list of textures
* @param scale amount to scale
*/
Sprite(const std::vector<sb::Texture>& textures, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
for (const sb::Texture& texture : textures)
{
this->texture(texture);
}
}
/*!
* Construct a ::Sprite object from a path to an image file which will converted into a texture.
*
* The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to
* the texture, and it must be loaded with a call to ::load or Texture::load.
*
* @param path path to an image
* @param scale amount to scale
*/
Sprite(const fs::path& path, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
this->texture(path);
}
/*!
* Construct a ::Sprite object from a list of paths to image files which will be converted into textures.
*
* @see ::Sprite(const fs::path, glm::vec3)
*
* @param paths list of paths to images
* @param scale amount to scale
*/
Sprite(const std::vector<fs::path>& paths, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
for (const fs::path& path : paths)
{
this->texture(path);
}
}
/*!
* Add a previously constructed sb::Texture to the ::Sprite object.
*
* @param texture sb::Texture object to add
*/
void texture(const sb::Texture& texture)
{
Model::texture(texture);
}
/*!
* Add a new texture to ::Sprite from a path to an image file which will converted into a new texture.
*
* The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to
* the texture, and it must be loaded with a call to ::load or Texture::load.
*
* @param path path to an image
*/
void texture(const fs::path& path)
{
sb::Texture texture;
if (SDL_GL_GetCurrentContext() != nullptr)
{
texture.load(path);
}
else
{
texture.associate(path);
}
this->texture(texture);
}
/*!
* Get a constant refernce to the texture at the given index. If no index is given, get the texture at index 0. If
* there are no textures, an exception will be thrown.
*
* @param index index of texture to get
*/
const sb::Texture& texture(int index = 0) const
{
return Model::texture(index);
}
/*!
*/
void translate(const glm::vec3& translation)
{
untransform();
_translation = Model::translate(translation);
transform();
}
/*!
* @param step amount to move sprite's translation transformation in three dimensions
*/
void move(const glm::vec3& step)
{
untransform();
_translation += Model::translate(step);
transform();
}
/*!
*/
void scale(const glm::vec2& scale)
{
untransform();
_scale = Model::scale({scale, 1.0f});
transform();
}
/*!
*/
void rotate(float angle, const glm::vec3& axis)
{
untransform();
_rotation = Model::rotate(angle, axis);
transform();
}
/*!
* The translation, scale, and rotation transformations, if previously set, are applied to the object's transformation
* property, along with the optional additional transformation matrix argument.
*
* The existing transformation property will be reset to the identity matrix before this transformation is applied.
*
* @warning This function works differently than Model::transform(const glm::mat4&). To apply an arbitrary transformation
* without having the non-arbitrary transformations applied as well, the rotate, scale, and translate transformations should
* be set to the identity matrix.
*
* @param transformation optional additional transformation to apply
*/
void transform(glm::mat4 transformation = glm::mat4{1.0f})
{
untransform();
Model::transform(_translation * _scale * _rotation * transformation);
}
/*!
* @return box object that represents the world coordinates of the rectangle surrounding the sprite object
*/
sb::Box box() const;
/*!
* @param sprite another sprite to check against for a collision
* @return true if the two sprite objects collide, false otherwise
*/
bool collide(const Sprite& sprite) const;
/*
*/
void bind_texture() const
{
if (textures().empty())
{
throw("Sprite doesn't have any textures to bind");
}
else
{
texture().bind();
}
}
/*
*/
void bind_texture(GLuint texture_flag_uniform) const
{
glUniform1i(texture_flag_uniform, true);
bind_texture();
}
/*
*/
void draw(GLuint transformation_uniform, const glm::mat4 view = glm::mat4{1.0f}, const glm::mat4 projection = glm::mat4{1.0f}) const
{
glm::mat4 mvp = projection * view * transformation();
glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]);
enable();
glDrawArrays(GL_TRIANGLES, 0, attributes("position")->count());
disable();
}
};
/*!
* A Pad is a Plane which can be clicked to launch an arbitrary user function. It can be sized and placed by setting its
* translation and scale values.
*
* Each instance:
*
* - Shares vertices and UV in VBO
* - Has its own Texture representing the button on-screen
* - Has its own response to click
* - Shares mouse collision code
* - Has its own translate + scale transformation
*
* Example:
*
* glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) *
* glm::vec3({-1, -1, 1});
* std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) *
* glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl;
* Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function<void()>()};
* const std::vector<glm::vec2>& p_position = *p.attributes("position");
* glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1};
* std::cout << p.transformation() << std::endl << final_position << std::endl;
* assert(final_position == glm::vec4({w.x, w.y, 0, 1}));
*/
template<typename return_type = void, typename... arguments>
class Pad : public sb::Plane
{
private:
using Reaction = std::function<return_type(bool, arguments...)>;
sb::Switch<return_type, arguments...> connection;
public:
Box collision_box;
/*!
* Construct a Pad object without a texture.
*
* @overload Pad(sb::Texture, glm::vec2, flat, float, Reaction, float)
*/
Pad(glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, Reaction on_state_change = Reaction(), float rotation = 0.0f)
{
if (translation != glm::vec2{0.0f, 0.0f})
{
this->translate(translation);
}
if (scale != 1.0f || ratio != 1.0f)
{
this->scale(scale, ratio);
}
if (rotation)
{
this->rotate(rotation);
}
this->on_state_change(on_state_change);
collision_box.invert_y(true);
collision_box.size({2.0f, 2.0f}, true);
}
/*!
* Construct a Pad from a texture, a translation amount, a scale factor, and a reaction function. The translation is relative
* to (0.0, 0.0), and the scale is relative to the superclass Plane object, which has opposite corners at (-1.0, -1.0) and
* (1.0, 1.0). The texture is the graphic that displays in the Pad location. The reaction must accept a boolean as its first
* argument, which will be the state of the contained Switch object.
*
* @param texture pad display graphic
* @param translation x, y amount to translate the pad position
* @param scale amount to scale both x and y
* @param ratio ratio to adjust scale of x and y
* @param on_state_change reaction function which accepts a boolean as its first argument
* @param rotation angle in radians to rotate the pad
*/
Pad(sb::Texture texture, glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f,
Reaction on_state_change = Reaction(), float rotation = 0.0f) : Pad(translation, scale, ratio, on_state_change, rotation)
{
this->texture(texture);
}
/*!
* Rotate the pad around its center by 90 degrees. If a count is given, rotate by 90 degrees that many times, so for example,
* a count of 3 will be a 270 degree rotation. If the count is negative, rotate -90 degrees.
*
* @param count number of 90 degree rotations to make, or use a negative count to rotate -90 degrees
*/
void rotate(int count = 1)
{
transform(glm::rotate(count * glm::half_pi<float>(), glm::vec3{0.0f, 0.0f, 1.0f}));
}
/*!
* Scale using a factor and ratio that will transform the pad in the X and Y dimensions. The ratio determines how much each axis
* is scaled. If the ratio is above one, the X-axis's scale will be divided by the ratio. If the ratio is below one, the Y-axis's
* scale will be multiplied by the aspect ratio. If the aspect ratio of the window is given, this will force the pad to display as
* a square, and the ratio will be relative to the shorter axis.
*
* The collision box will be scaled by the same factors.
*
* @param factor amount to scale in both x and y directions
* @param ratio amount to adjust scaling, set to the window aspect ratio to make the pad appear square
*/
const glm::mat4& scale(float factor, float ratio = 1.0f)
{
glm::vec3 scale { factor, factor, 1.0f };
if (ratio > 1.0f)
{
scale.x /= ratio;
}
else if (ratio < 1.0f)
{
scale.y *= ratio;
}
collision_box.scale({scale.x, scale.y}, true);
return sb::Plane::scale(scale);
}
const glm::mat4& scale(const glm::vec3& scale)
{
collision_box.scale({scale.x, scale.y}, true);
return sb::Plane::scale(scale);
}
/*!
* Move the pad in the X and Y dimension using a 2D vector. The collision box will be moved by the same translation.
*
* @param translation x, y distance to translate the pad
*/
void translate(const glm::vec2& translation)
{
collision_box.move(translation);
sb::Plane::translate({translation.x, translation.y, 0.0f});
}
/*!
* Set the function that will run when a pad object is clicked.
*
* @param on_state_change reaction function which accepts a boolean as its first argument
*/
void on_state_change(Reaction reaction)
{
connection.on_state_change(reaction);
}
/*!
* Returns true if the point at given position collides with the pad's collision box.
*
* @param position x, y coordinate to check for collision with Pad::collision_box
* @return true if the point is inside Pad::collision_box, false otherwise
*/
bool collide(const glm::vec2& position) const
{
return collision_box.collide(position);
}
/*!
* Set the transformation uniform, bind the texture, and draw the vertices. Build the full transformation matrix
* by combining the Pad object's transformation with the supplied projection and view matrices, and pass it to
* the shader.
*
* The active texture must have been set with its ID passed to the corresponding shader uniform previously.
*
* @param uniform_id transformation uniform ID
* @param texture_flag_uniform_id uniform ID for boolean enabling or disabling texture display
* @param view the view matrix for transforming from world space to camera space
* @param projection projection matrix for transforming from camera space to clip space
*/
void draw(GLuint uniform_id, GLuint texture_flag_uniform_id, glm::mat4 view, glm::mat4 projection)
{
if (!textures().empty())
{
glUniform1i(texture_flag_uniform_id, true);
texture().bind();
}
else
{
glUniform1i(texture_flag_uniform_id, false);
}
glm::mat4 mvp = projection * view * transformation();
glUniformMatrix4fv(uniform_id, 1, GL_FALSE, &mvp[0][0]);
enable();
glDrawArrays(GL_TRIANGLES, 0, attributes("position")->count());
disable();
}
const glm::mat4& untransform()
{
collision_box.size({2.0f, 2.0f}, true);
return sb::Plane::untransform();
}
return_type press(arguments... args)
{
return connection.flip(args...);
}
};
class Curve
{
public:
float aspect;
std::vector<glm::vec2> unwrapped;
std::vector<sb::Attributes> position;
sb::Attributes color;
Curve (float aspect) : aspect(aspect) {}
void add(const std::vector<glm::vec2>& vertices)
{
unwrapped = vertices;
for (const std::vector<glm::vec2>& wrapped : sb::wrap_curve(vertices, {-aspect, -1.0f}, {aspect, 1.0f}))
{
position.push_back(sb::Attributes(wrapped));
for (std::size_t jj = 0; jj < wrapped.size(); jj++)
{
color.add(1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
/*!
* @return number of vertices in the unwrapped curve
*/
int length() const;
/*!
* @return the first point of the unwrapped curve
*/
const glm::vec2& front() const;
/*!
* @return size in bytes of the GL vertex data
*/
std::size_t size() const
{
std::size_t byte_count = 0;
for (const auto& attr : position)
{
byte_count += attr.size();
}
byte_count += color.size();
return byte_count;
}
/*!
* @param index index of the vertex in the unwrapped curve vertices list
* @return vertex in the unwrapped vertices list at index
*/
const glm::vec2& operator[](int index) const;
};
class Character
{
private:
inline static const std::vector<std::string> PROFILES = {"cake", "ball", "buffalo"};
Configuration& configuration;
float speed = 0.0f;
int next_point_index = 0;
/*!
* @param suffix the character profile config value to get, without the character name
* @return character profile config value
*/
const nlohmann::json& entry(const std::string& suffix) const;
public:
sb::Switch<> accelerating = false;
sb::Selection<std::vector<std::string>> profile = PROFILES;
glm::vec2 position;
Character(Configuration& configuration) : configuration(configuration) {}
/*!
* @param curve the curve to reset the beginning position to
*/
void reset(const Curve& curve);
/*!
* Check acceleration state and adjust speed. Move character toward the next point on the curve.
*
* @param curve the curve to update against
*/
void update(const Curve& curve, const sb::Timer& timer);
};
/* Project classes */
#include "Character.hpp"
#include "Sprite.hpp"
#include "Pad.hpp"
#include "Curve.hpp"
/*!
* The main game object. There is currently only support for one of these to exist at a time.
@ -596,12 +92,12 @@ private:
Pad<> field;
sb::Timer timer;
Character character{_configuration};
Sprite character_sprite;
glm::vec3 camera_position {0.0f, 0.0f, 2.0f}, subject_position {0.0f, 0.0f, 0.0f};
float field_of_view_y = 2 * glm::atan(1.0 / camera_position.z);
glm::vec2 rotation = {0.0f, 0.0f};
sb::Text fps{font()};
std::vector<Curve> curves;
std::vector<Sprite> enemies;
/*!
* Create GL context via super class and load vertices, UV data, and shaders.

77
src/Character.cpp Normal file
View File

@ -0,0 +1,77 @@
#include "Character.hpp"
void Character::reset(const Curve& curve)
{
next_point_index = 0;
speed = 0.0f;
position = curve.front();
accelerating = false;
}
const nlohmann::json& Character::entry(const std::string& suffix) const
{
return configuration["character"][*profile.current() + "-" + suffix];
}
void Character::update(const Curve& curve, const sb::Timer& timer)
{
/* Adjust speed based on acceleration state and character profile. */
if (accelerating)
{
/* Apply delta time to the speed increase. */
speed += timer.delta(entry("speed-increment").get<float>()) + glm::abs(speed) * entry("increment-mod").get<float>();
}
else
{
/* Apply delta time to the speed decrease. */
speed -= timer.delta(entry("speed-decrement").get<float>()) + glm::abs(speed) * entry("decrement-mod").get<float>();
}
/* Clamp speed, applying delta time to the limits */
if (speed > timer.delta(entry("max-speed").get<float>()))
{
speed = timer.delta(entry("max-speed").get<float>());
}
else if (speed < timer.delta(entry("min-speed").get<float>()))
{
speed = timer.delta(entry("min-speed").get<float>());
}
/* Move along unwrapped curve vertices */
float distance_remaining = std::abs(speed), distance = 0.0f;
glm::vec2 next_point, step;
while (distance_remaining)
{
if (speed < 0.0f && next_point_index == 0)
{
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 = sb::Segment(position, next_point).step(distance_remaining);
position += step;
distance_remaining = 0;
}
}
}

47
src/Character.hpp Normal file
View File

@ -0,0 +1,47 @@
#include <vector>
#include "Configuration.hpp"
#include "Switch.hpp"
#include "Selection.hpp"
#include "Sprite.hpp"
#include "Curve.hpp"
#include "Segment.hpp"
class Character
{
private:
inline static const std::vector<std::string> PROFILES = {"cake", "ball", "buffalo"};
Configuration& configuration;
float speed = 0.0f;
int next_point_index = 0;
/*!
* @param suffix the character profile config value to get, without the character name
* @return character profile config value
*/
const nlohmann::json& entry(const std::string& suffix) const;
public:
sb::Switch<> accelerating = false;
sb::Selection<std::vector<std::string>> profile = PROFILES;
glm::vec2 position;
Sprite sprite;
Character(Configuration& configuration) : configuration(configuration) {}
/*!
* @param curve the curve to reset the beginning position to
*/
void reset(const Curve& curve);
/*!
* Check acceleration state and adjust speed. Move character toward the next point on the curve.
*
* @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
*/
void update(const Curve& curve, const sb::Timer& timer);
};

16
src/Curve.cpp Normal file
View File

@ -0,0 +1,16 @@
#include "Curve.hpp"
int Curve::length() const
{
return unwrapped.size();
}
const glm::vec2& Curve::front() const
{
return unwrapped.front();
}
const glm::vec2& Curve::operator[](int index) const
{
return unwrapped[index];
}

62
src/Curve.hpp Normal file
View File

@ -0,0 +1,62 @@
#pragma once
#include <vector>
#include "glm/glm.hpp"
#include "Attributes.hpp"
#include "math.hpp"
class Curve
{
public:
float aspect;
std::vector<glm::vec2> unwrapped;
std::vector<sb::Attributes> position;
sb::Attributes color;
Curve (float aspect) : aspect(aspect) {}
void add(const std::vector<glm::vec2>& vertices)
{
unwrapped = vertices;
for (const std::vector<glm::vec2>& wrapped : sb::wrap_curve(vertices, {-aspect, -1.0f}, {aspect, 1.0f}))
{
position.push_back(sb::Attributes(wrapped));
for (std::size_t jj = 0; jj < wrapped.size(); jj++)
{
color.add(1.0f, 1.0f, 1.0f, 1.0f);
}
}
}
/*!
* @return number of vertices in the unwrapped curve
*/
int length() const;
/*!
* @return the first point of the unwrapped curve
*/
const glm::vec2& front() const;
/*!
* @return size in bytes of the GL vertex data
*/
std::size_t size() const
{
std::size_t byte_count = 0;
for (const auto& attr : position)
{
byte_count += attr.size();
}
byte_count += color.size();
return byte_count;
}
/*!
* @param index index of the vertex in the unwrapped curve vertices list
* @return vertex in the unwrapped vertices list at index
*/
const glm::vec2& operator[](int index) const;
};

202
src/Pad.hpp Normal file
View File

@ -0,0 +1,202 @@
#pragma once
#include <functional>
#include "Model.hpp"
#include "Switch.hpp"
/*!
* A Pad is a Plane which can be clicked to launch an arbitrary user function. It can be sized and placed by setting its
* translation and scale values.
*
* Each instance:
*
* - Shares vertices and UV in VBO
* - Has its own Texture representing the button on-screen
* - Has its own response to click
* - Shares mouse collision code
* - Has its own translate + scale transformation
*
* Example:
*
* glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) *
* glm::vec3({-1, -1, 1});
* std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) *
* glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl;
* Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function<void()>()};
* const std::vector<glm::vec2>& p_position = *p.attributes("position");
* glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1};
* std::cout << p.transformation() << std::endl << final_position << std::endl;
* assert(final_position == glm::vec4({w.x, w.y, 0, 1}));
*/
template<typename return_type = void, typename... arguments>
class Pad : public sb::Plane
{
private:
using Reaction = std::function<return_type(bool, arguments...)>;
sb::Switch<return_type, arguments...> connection;
public:
Box collision_box;
/*!
* Construct a Pad object without a texture.
*
* @overload Pad(sb::Texture, glm::vec2, flat, float, Reaction, float)
*/
Pad(glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f, Reaction on_state_change = Reaction(), float rotation = 0.0f)
{
if (translation != glm::vec2{0.0f, 0.0f})
{
this->translate(translation);
}
if (scale != 1.0f || ratio != 1.0f)
{
this->scale(scale, ratio);
}
if (rotation)
{
this->rotate(rotation);
}
this->on_state_change(on_state_change);
collision_box.invert_y(true);
collision_box.size({2.0f, 2.0f}, true);
}
/*!
* Construct a Pad from a texture, a translation amount, a scale factor, and a reaction function. The translation is relative
* to (0.0, 0.0), and the scale is relative to the superclass Plane object, which has opposite corners at (-1.0, -1.0) and
* (1.0, 1.0). The texture is the graphic that displays in the Pad location. The reaction must accept a boolean as its first
* argument, which will be the state of the contained Switch object.
*
* @param texture pad display graphic
* @param translation x, y amount to translate the pad position
* @param scale amount to scale both x and y
* @param ratio ratio to adjust scale of x and y
* @param on_state_change reaction function which accepts a boolean as its first argument
* @param rotation angle in radians to rotate the pad
*/
Pad(sb::Texture texture, glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f,
Reaction on_state_change = Reaction(), float rotation = 0.0f) : Pad(translation, scale, ratio, on_state_change, rotation)
{
this->texture(texture);
}
/*!
* Rotate the pad around its center by 90 degrees. If a count is given, rotate by 90 degrees that many times, so for example,
* a count of 3 will be a 270 degree rotation. If the count is negative, rotate -90 degrees.
*
* @param count number of 90 degree rotations to make, or use a negative count to rotate -90 degrees
*/
void rotate(int count = 1)
{
transform(glm::rotate(count * glm::half_pi<float>(), glm::vec3{0.0f, 0.0f, 1.0f}));
}
/*!
* Scale using a factor and ratio that will transform the pad in the X and Y dimensions. The ratio determines how much each axis
* is scaled. If the ratio is above one, the X-axis's scale will be divided by the ratio. If the ratio is below one, the Y-axis's
* scale will be multiplied by the aspect ratio. If the aspect ratio of the window is given, this will force the pad to display as
* a square, and the ratio will be relative to the shorter axis.
*
* The collision box will be scaled by the same factors.
*
* @param factor amount to scale in both x and y directions
* @param ratio amount to adjust scaling, set to the window aspect ratio to make the pad appear square
*/
const glm::mat4& scale(float factor, float ratio = 1.0f)
{
glm::vec3 scale { factor, factor, 1.0f };
if (ratio > 1.0f)
{
scale.x /= ratio;
}
else if (ratio < 1.0f)
{
scale.y *= ratio;
}
collision_box.scale({scale.x, scale.y}, true);
return sb::Plane::scale(scale);
}
const glm::mat4& scale(const glm::vec3& scale)
{
collision_box.scale({scale.x, scale.y}, true);
return sb::Plane::scale(scale);
}
/*!
* Move the pad in the X and Y dimension using a 2D vector. The collision box will be moved by the same translation.
*
* @param translation x, y distance to translate the pad
*/
void translate(const glm::vec2& translation)
{
collision_box.move(translation);
sb::Plane::translate({translation.x, translation.y, 0.0f});
}
/*!
* Set the function that will run when a pad object is clicked.
*
* @param on_state_change reaction function which accepts a boolean as its first argument
*/
void on_state_change(Reaction reaction)
{
connection.on_state_change(reaction);
}
/*!
* Returns true if the point at given position collides with the pad's collision box.
*
* @param position x, y coordinate to check for collision with Pad::collision_box
* @return true if the point is inside Pad::collision_box, false otherwise
*/
bool collide(const glm::vec2& position) const
{
return collision_box.collide(position);
}
/*!
* Set the transformation uniform, bind the texture, and draw the vertices. Build the full transformation matrix
* by combining the Pad object's transformation with the supplied projection and view matrices, and pass it to
* the shader.
*
* The active texture must have been set with its ID passed to the corresponding shader uniform previously.
*
* @param uniform_id transformation uniform ID
* @param texture_flag_uniform_id uniform ID for boolean enabling or disabling texture display
* @param view the view matrix for transforming from world space to camera space
* @param projection projection matrix for transforming from camera space to clip space
*/
void draw(GLuint uniform_id, GLuint texture_flag_uniform_id, glm::mat4 view, glm::mat4 projection)
{
if (!textures().empty())
{
glUniform1i(texture_flag_uniform_id, true);
texture().bind();
}
else
{
glUniform1i(texture_flag_uniform_id, false);
}
glm::mat4 mvp = projection * view * transformation();
glUniformMatrix4fv(uniform_id, 1, GL_FALSE, &mvp[0][0]);
enable();
glDrawArrays(GL_TRIANGLES, 0, attributes("position")->count());
disable();
}
const glm::mat4& untransform()
{
collision_box.size({2.0f, 2.0f}, true);
return sb::Plane::untransform();
}
return_type press(arguments... args)
{
return connection.flip(args...);
}
};

223
src/Sprite.hpp Normal file
View File

@ -0,0 +1,223 @@
#pragma once
#include "glm/glm.hpp"
#include "Model.hpp"
class Sprite : private sb::Plane
{
private:
/* Keep a reference of the matrix transformations generated when the user applys a transformation, so that each can be reapplied
* without having to set all the transformations every time one is changed. */
glm::mat4 _scale {1.0f}, _translation {1.0f}, _rotation {1.0f};
public:
/*!
* Construct a Sprite with an optional scale amount.
*
* @param scale amount to scale
*/
Sprite(glm::vec2 scale = glm::vec2{1.0f})
{
this->scale(scale);
}
/*!
* Construct a Sprite from a texture. The texture is the 2D graphic that displays in the Sprite's location.
*
* @param texture sprite's 2D graphic
* @param scale amount to scale
*/
Sprite(const sb::Texture& texture, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
this->texture(texture);
}
/*!
* Construct a Sprite from a list of textures. Each texture is a frame of the Sprite's animation.
*
* @param textures list of textures
* @param scale amount to scale
*/
Sprite(const std::vector<sb::Texture>& textures, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
for (const sb::Texture& texture : textures)
{
this->texture(texture);
}
}
/*!
* Construct a ::Sprite object from a path to an image file which will converted into a texture.
*
* The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to
* the texture, and it must be loaded with a call to ::load or Texture::load.
*
* @param path path to an image
* @param scale amount to scale
*/
Sprite(const fs::path& path, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
this->texture(path);
}
/*!
* Construct a ::Sprite object from a list of paths to image files which will be converted into textures.
*
* @see ::Sprite(const fs::path, glm::vec3)
*
* @param paths list of paths to images
* @param scale amount to scale
*/
Sprite(const std::vector<fs::path>& paths, glm::vec2 scale = glm::vec2{1.0f}) : Sprite(scale)
{
for (const fs::path& path : paths)
{
this->texture(path);
}
}
/*!
* Add a previously constructed sb::Texture to the ::Sprite object.
*
* @param texture sb::Texture object to add
*/
void texture(const sb::Texture& texture)
{
Model::texture(texture);
}
/*!
* Add a new texture to ::Sprite from a path to an image file which will converted into a new texture.
*
* The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to
* the texture, and it must be loaded with a call to ::load or Texture::load.
*
* @param path path to an image
*/
void texture(const fs::path& path)
{
sb::Texture texture;
if (SDL_GL_GetCurrentContext() != nullptr)
{
texture.load(path);
}
else
{
texture.associate(path);
}
this->texture(texture);
}
/*!
* Get a constant refernce to the texture at the given index. If no index is given, get the texture at index 0. If
* there are no textures, an exception will be thrown.
*
* @param index index of texture to get
*/
const sb::Texture& texture(int index = 0) const
{
return Model::texture(index);
}
/*!
*/
void translate(const glm::vec3& translation)
{
untransform();
_translation = Model::translate(translation);
transform();
}
/*!
* @param step amount to move sprite's translation transformation in three dimensions
*/
void move(const glm::vec3& step)
{
untransform();
_translation += Model::translate(step);
transform();
}
/*!
*/
void scale(const glm::vec2& scale)
{
untransform();
_scale = Model::scale({scale, 1.0f});
transform();
}
/*!
*/
void rotate(float angle, const glm::vec3& axis)
{
untransform();
_rotation = Model::rotate(angle, axis);
transform();
}
/*!
* The translation, scale, and rotation transformations, if previously set, are applied to the object's transformation
* property, along with the optional additional transformation matrix argument.
*
* The existing transformation property will be reset to the identity matrix before this transformation is applied.
*
* @warning This function works differently than Model::transform(const glm::mat4&). To apply an arbitrary transformation
* without having the non-arbitrary transformations applied as well, the rotate, scale, and translate transformations should
* be set to the identity matrix.
*
* @param transformation optional additional transformation to apply
*/
void transform(glm::mat4 transformation = glm::mat4{1.0f})
{
untransform();
Model::transform(_translation * _scale * _rotation * transformation);
}
/*!
* @return box object that represents the world coordinates of the rectangle surrounding the sprite object
*/
sb::Box box() const;
/*!
* @param sprite another sprite to check against for a collision
* @return true if the two sprite objects collide, false otherwise
*/
bool collide(const Sprite& sprite) const;
/*
*/
void bind_texture() const
{
if (textures().empty())
{
throw("Sprite doesn't have any textures to bind");
}
else
{
texture().bind();
}
}
/*
*/
void bind_texture(GLuint texture_flag_uniform) const
{
glUniform1i(texture_flag_uniform, true);
bind_texture();
}
/*
*/
void draw(GLuint transformation_uniform, const glm::mat4 view = glm::mat4{1.0f}, const glm::mat4 projection = glm::mat4{1.0f}) const
{
glm::mat4 mvp = projection * view * transformation();
glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]);
enable();
glDrawArrays(GL_TRIANGLES, 0, attributes("position")->count());
disable();
}
};