spacebox/src/Pad.hpp

353 lines
14 KiB
C++

/* +-------------------------------------------------------+
____/ \____ /| Open source game framework licensed to freely use, |
\ / / | copy, and modify, created for dank.game |
+--\ ^__^ /--+ | |
| ~/ \~ | | Download at https://open.shampoo.ooo/shampoo/spacebox |
| ~~~~~~~~~~~~ | +-------------------------------------------------------+
| SPACE ~~~~~ | /
| ~~~~~~~ BOX |/
+-------------*/
#pragma once
#include <optional>
#include <functional>
#include "Model.hpp"
#include "Switch.hpp"
#include "math.hpp"
namespace sb
{
/*!
* A Pad is an object containing an sb::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:
*
* - Has an sb::Plane or derivative (either a custom one provided, or a default constructed one automatically)
* - Has an arbitrary response function for when pressed
* - Can collide with a point (for example, mouse click)
*
* 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 ReturnType = void, typename... Arguments>
class Pad
{
private:
using Reaction = std::function<ReturnType(bool, Arguments...)>;
sb::Switch<ReturnType, Arguments...> connection;
sb::Plane _plane;
Box _box;
int texture_index = 0;
bool _enabled = true, _visible = true;
public:
/*!
* Construct a Pad object with a default constructed sb::Plane.
*
* @see Pad(sb::Plane, const glm::vec2&, float, float, Reaction, float)
*/
Pad(const 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(sb::Plane(), translation, scale, ratio, on_state_change, rotation) {}
/*!
* Construct a pad object from an sb::Plane, 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 plane object.
*
* The reaction function must accept a boolean as its first argument, which will be given the state of the pad object's switch.
*
* The plane object will be copied into the pad, so any edits must be made before constructing the pad, unless there is a Pad
* function that applies the edit, such as Pad::scale(float, float).
*
* @param plane plane or plane derivative to represent the pad visually
* @param translation x, y amount to translate the 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(const sb::Plane& plane, glm::vec2 translation = {0.0f, 0.0f}, float scale = 1.0f, float ratio = 1.0f,
Reaction on_state_change = Reaction(), float rotation = 0.0f) : _plane(plane)
{
_box.size({2.0f, 2.0f}, true);
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);
}
/*!
* Assign a new plane to the pad. The existing plane will be replaced.
*
* @param plane plane or plane derivative to represent the pad visually
*/
void plane(const sb::Plane& plane)
{
_plane = plane;
}
/*!
* @return Constant reference to the pad's plane object
*/
const sb::Plane& plane() const
{
return _plane;
}
/*!
* @return Constant reference to the pad's box object
*/
const sb::Box& box() const
{
return _box;
}
/*!
* 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)
{
_plane.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;
}
_box.size({2.0f, 2.0f}, true);
_box.scale({scale.x, scale.y}, true);
return _plane.scale(scale);
}
/*!
* Set the pad's location 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
*/
const glm::mat4& translate(const glm::vec2& translation)
{
_box.center({0.0f, 0.0f});
_box.move(translation);
return _plane.translate({translation.x, translation.y, 0.0f});
}
/*!
* Set the function that will run when a pad object is clicked.
*
* This example always keeps the state false and prints "Hello, World!" whenever the pad is clicked,
*
* start_button.on_state_change([&](bool state, int count){
* if (state) {
* std::ostringstream message;
* message << "Hello, " << state << " World! " << count;
* sb::Log::log(message);
* start_button.press(1);
* }});
*
* @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 NDC coordinates collides with the pad's collision box. This only works properly if the
* Pad is flat in the z-dimension.
*
* @param position x, y NDC coordinates to check for collision
* @return true if the point is inside Pad::box, false otherwise
*/
bool collide(const glm::vec2& position, const glm::mat4& view, const glm::mat4& projection) const
{
/* Corners of the box */
std::vector<glm::vec3> corners;
/* Transform each of the corners into NDC coordinates */
for (const glm::vec2& vertex : {_box.sw(), _box.nw(), _box.ne(), _box.se()})
{
corners.push_back(sb::world_to_ndc(vertex, projection * view));
}
/* Create a new box using the NDC bottom-left corner */
glm::vec2 control = corners[0];
float width = glm::distance(corners[3], corners[0]);
float height = glm::distance(corners[1], corners[0]);
sb::Box transformed {control, {width, height}};
/* Collide the NDC box with the caller's coordinates */
return transformed.collide(position);
}
/*!
* Set the transformation uniform, bind a texture if any are attached, 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.
*
* @param transformation_uniform transformation uniform ID
* @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
* @param texture_flag_uniform uniform ID for boolean enabling or disabling texture display
*/
void draw(GLuint transformation_uniform, glm::mat4 view, glm::mat4 projection,
std::optional<GLuint> texture_flag_uniform = std::nullopt)
{
if (_visible)
{
if (!_plane.textures().empty())
{
if (texture_flag_uniform.has_value())
{
glUniform1i(texture_flag_uniform.value(), true);
}
/* Determine texture index by checking the state of the pad and the amount of available textures. If there is more
* than 1 texture, the texture will correspond with the state. */
if (connection && _plane.textures().size() > 1)
{
texture_index = 1;
}
else
{
texture_index = 0;
}
_plane.texture(texture_index).bind();
}
else if (texture_flag_uniform.has_value())
{
glUniform1i(texture_flag_uniform.value(), false);
}
glm::mat4 mvp = projection * view * _plane.transformation();
glUniformMatrix4fv(transformation_uniform, 1, GL_FALSE, &mvp[0][0]);
_plane.enable();
glDrawArrays(GL_TRIANGLES, 0, _plane.attributes("position")->count());
_plane.disable();
}
}
/*!
* Run the reaction function.
*
* @param args Arguments to pass to the reaction function
* @exception std::runtime_error if the pad is currently disabled
* @return Result of the reaction function if it returns a value, or void otherwise
*/
ReturnType press(Arguments... args)
{
if (!_enabled)
{
throw std::runtime_error(
"The pad cannot be pressed because it is currently disabled. Please check Pad::enabled before calling Pad::press.");
}
else
{
return connection.flip(args...);
}
}
/*!
* @return the state of the pad
*/
bool pressed() const
{
return connection;
}
/*!
* This will not trigger the callback if any is set. To do that, use Pad::press(Arguments...).
*
* @param state true or false to set the pad state
*/
void state(bool pressed)
{
connection = pressed;
}
/*!
* @return size in bytes of the pad object's plane object
*/
std::size_t size() const
{
return _plane.size();
}
/*!
* By default, a pad object is enabled to accept input presses. If the pad is disabled, however, the press function will not
* be able to be used and will throw an exception. This function can be used both to check the state and to set the state.
*
* @param state Set to false to disable, true to enable, or omit to check the current state
* @return True if button is set to enabled
*/
bool enabled(std::optional<bool> state = std::nullopt)
{
if (state.has_value())
{
_enabled = state.value();
}
return _enabled;
}
/*!
* Use this function to prevent the draw function from running, which will prevent the pad object from being rendered. Note
* that this does not disable input. To do that, use Pad::enabled(bool)
*
* @param state Set to false to prevent the pad from being drawn, set to true to re-enable drawing
*/
bool visible(bool state)
{
_visible = state;
return _visible;
}
/*!
* @return the pad's visibility state
*/
bool visible() const
{
return _visible;
}
};
}