552 lines
17 KiB
C++
552 lines
17 KiB
C++
#include "AudioDevice.hpp"
|
|
#include "Color.hpp"
|
|
#include "Keyboard.hpp"
|
|
#include "Matrix.hpp"
|
|
#include "Mesh.hpp"
|
|
#include "Model.hpp"
|
|
#include "RadiansDegrees.hpp"
|
|
#include "Vector3.hpp"
|
|
#include "raylib.h"
|
|
#include <BufferedRaylib.hpp>
|
|
#include <raylib-cpp.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <concepts>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#define SKYBOX_IMPLEMENTATION
|
|
#include "skybox.hpp"
|
|
|
|
raylib::Degree angle_normalize(raylib::Degree angle) {
|
|
float decimal = float(angle) - int(angle);
|
|
int normalized = (int(angle) % 360 + 360) % 360;
|
|
return raylib::Degree(normalized + decimal);
|
|
}
|
|
|
|
float shortest_angle_delta(raylib::Degree from, raylib::Degree to) {
|
|
float diff = std::fmod(float(to) - float(from) + 180.0f, 360.0f);
|
|
if (diff < 0.0f) {
|
|
diff += 360.0f;
|
|
}
|
|
return diff - 180.0f;
|
|
}
|
|
|
|
raylib::Vector3 velocity_from_speed_heading(float speed, raylib::Degree heading) {
|
|
return raylib::Vector3{speed * std::sin(heading.RadianValue()), 0.0f,
|
|
speed * std::cos(heading.RadianValue())};
|
|
}
|
|
|
|
void DrawBoundedModel(raylib::Model &model, bool drawBoundingBox, auto transformer) {
|
|
// store the original transform to apply a different transform to the
|
|
// model without affecting the next time we draw
|
|
raylib::Matrix oldTransform = model.GetTransform();
|
|
|
|
// apply the transform that we get from whatever the transformer callback
|
|
// gives us
|
|
raylib::Matrix transform = transformer(model.GetTransform());
|
|
|
|
// apply the transform that we got from the transformer to the model
|
|
model.SetTransform(transform);
|
|
|
|
// draw the model, passing the origin and default scale as arguments since
|
|
// the transform is already applied to the model
|
|
model.Draw({0, 0, 0}, 1.0f, raylib::Color::White());
|
|
|
|
// get the bounding box of the model after applying the transform
|
|
auto box = model.GetTransformedBoundingBox();
|
|
|
|
// draw the bounding box of the model using raylib's built in function
|
|
if (drawBoundingBox) {
|
|
DrawBoundingBox(box, raylib::Color::White());
|
|
}
|
|
|
|
// restore the model's transform to its original state so that the next time we
|
|
// draw the model, it doesn't have the previous transform applied to it
|
|
model.SetTransform(oldTransform);
|
|
}
|
|
|
|
struct Entity;
|
|
class Component;
|
|
|
|
class Component {
|
|
public:
|
|
Entity *entity = nullptr;
|
|
|
|
virtual void Setup() = 0;
|
|
virtual void Update(float dt) = 0;
|
|
virtual void Cleanup() = 0;
|
|
|
|
virtual ~Component() = default;
|
|
};
|
|
|
|
struct Entity {
|
|
std::vector<std::shared_ptr<Component>> components;
|
|
|
|
template <std::derived_from<Component> T> T &AddComponent() {
|
|
auto &ptr = components.emplace_back(std::make_shared<T>());
|
|
ptr->entity = this;
|
|
ptr->Setup();
|
|
return static_cast<T &>(*ptr);
|
|
}
|
|
|
|
template <std::derived_from<Component> T>
|
|
std::optional<std::reference_wrapper<T>> GetComponent() const {
|
|
for (auto &c : components) {
|
|
T *cast = dynamic_cast<T *>(c.get());
|
|
if (cast) {
|
|
return *cast;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void Update(float dt) {
|
|
for (auto &c : components) {
|
|
c->Update(dt);
|
|
}
|
|
}
|
|
};
|
|
|
|
template <typename T> struct Delegate {};
|
|
|
|
template <typename TReturn, typename... TArgs> struct Delegate<TReturn(TArgs...)> {
|
|
std::vector<std::function<void(int)>> functions;
|
|
|
|
void connect(const std::function<TReturn(TArgs...)> &func) { functions.push_back(func); }
|
|
|
|
void operator+=(const std::function<TReturn(TArgs...)> &func) { connect(func); }
|
|
|
|
void operator()(int arg) {
|
|
static_assert(std::is_same_v<TReturn, void>,
|
|
"EC HW: Delegate only supports void return type");
|
|
|
|
for (const auto &func : functions) {
|
|
f(std::forward<TArgs>(arg)...);
|
|
}
|
|
}
|
|
};
|
|
|
|
struct TransformComponent : public Component {
|
|
raylib::Vector3 position = {0, 0, 0};
|
|
raylib::Degree heading = 0;
|
|
|
|
void Setup() override {}
|
|
void Update(float dt) override {}
|
|
void Cleanup() override {}
|
|
};
|
|
|
|
struct RenderComponent : public Component {
|
|
raylib::Model *model = nullptr;
|
|
bool drawBoundingBox = false;
|
|
float modelScale = 40.0f;
|
|
|
|
void Setup() override {}
|
|
void Cleanup() override {}
|
|
|
|
void Update(float dt) override {
|
|
if (!model) {
|
|
return;
|
|
}
|
|
|
|
auto t = entity->GetComponent<TransformComponent>();
|
|
if (!t) {
|
|
return;
|
|
}
|
|
|
|
TransformComponent &tf = t->get();
|
|
|
|
DrawBoundedModel(*model, drawBoundingBox, [&tf, this](raylib::Matrix transform) {
|
|
return transform.RotateY(tf.heading)
|
|
.Scale(modelScale, modelScale, modelScale)
|
|
.Translate(tf.position);
|
|
});
|
|
}
|
|
};
|
|
|
|
struct PhysicsComponent : public Component {
|
|
raylib::Vector3 velocity = {0, 0, 0};
|
|
float maxSpeed = 300.0f;
|
|
float acceleration = 100.0f;
|
|
float turningRate = 90.0f;
|
|
|
|
void Setup() override {}
|
|
void Cleanup() override {}
|
|
|
|
void Update(float dt) override {
|
|
auto t = entity->GetComponent<TransformComponent>();
|
|
if (!t) {
|
|
return;
|
|
}
|
|
t->get().position = t->get().position + velocity * dt;
|
|
}
|
|
};
|
|
|
|
struct OrientedPhysicsComponent : public PhysicsComponent {
|
|
float speed = 0.0f;
|
|
|
|
void Setup() override {}
|
|
void Cleanup() override {}
|
|
|
|
void Update(float dt) override {
|
|
auto t = entity->GetComponent<TransformComponent>();
|
|
if (!t) {
|
|
return;
|
|
}
|
|
|
|
TransformComponent &tf = t->get();
|
|
|
|
speed = std::clamp(speed, -maxSpeed, maxSpeed);
|
|
|
|
velocity = velocity_from_speed_heading(speed, tf.heading);
|
|
|
|
tf.position = tf.position + velocity * dt;
|
|
}
|
|
};
|
|
|
|
struct FollowComponent : public Component {
|
|
Entity *target = nullptr;
|
|
float hoverHeight = 200.0f;
|
|
float speed = 0.0f;
|
|
float maxSpeed = 250.0f;
|
|
float acceleration = 80.0f;
|
|
float turningRate = 120.0f;
|
|
float epsilon = 8.0f;
|
|
|
|
void Setup() override {}
|
|
void Cleanup() override {}
|
|
|
|
void Update(float dt) override {
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto myT = entity->GetComponent<TransformComponent>();
|
|
auto targetT = target->GetComponent<TransformComponent>();
|
|
if (!myT || !targetT) {
|
|
return;
|
|
}
|
|
|
|
TransformComponent &myTf = myT->get();
|
|
TransformComponent &targetTf = targetT->get();
|
|
|
|
raylib::Vector3 desired = {targetTf.position.x, targetTf.position.y + hoverHeight,
|
|
targetTf.position.z};
|
|
|
|
raylib::Vector3 toDesired = desired - myTf.position;
|
|
float distXZ = std::sqrt(toDesired.x * toDesired.x + toDesired.z * toDesired.z);
|
|
|
|
if (distXZ > epsilon) {
|
|
float desiredHeadingRad = std::atan2(toDesired.x, toDesired.z);
|
|
raylib::Degree desiredHeading = raylib::Degree(desiredHeadingRad * RAD2DEG);
|
|
|
|
float diff = float(desiredHeading) - float(myTf.heading);
|
|
|
|
// norm to [-180, 180] for shortest turn direction
|
|
angle_normalize(raylib::Degree(diff + 180) - raylib::Degree(180));
|
|
|
|
float maxTurn = turningRate * dt;
|
|
float turn = std::clamp(diff, -maxTurn, maxTurn);
|
|
myTf.heading = angle_normalize(myTf.heading + raylib::Degree(turn));
|
|
|
|
speed = std::min(speed + acceleration * dt, maxSpeed);
|
|
} else {
|
|
speed = std::max(speed - acceleration * dt, 0.0f);
|
|
}
|
|
|
|
raylib::Vector3 move = velocity_from_speed_heading(speed, myTf.heading);
|
|
|
|
myTf.position.x += move.x * dt;
|
|
myTf.position.z += move.z * dt;
|
|
|
|
float yDiff = desired.y - myTf.position.y;
|
|
myTf.position.y += yDiff * 5.0f * dt;
|
|
}
|
|
};
|
|
|
|
struct InputComponent : public Component {
|
|
raylib::BufferedInput *input = nullptr;
|
|
bool isActive = false;
|
|
|
|
bool wishInitialized = false;
|
|
float wishSpeed = 0.0f;
|
|
raylib::Degree wishHeading = 0.0f;
|
|
|
|
// these do not actually indicate any acceleration or angular velocity, it
|
|
// just updates the target velocity/heading, but the rate the character
|
|
// turns to this target is limited by its PhysicsComponent
|
|
float speedStep = 40.0f;
|
|
float headingStep = 18.0f;
|
|
|
|
void Setup() override {
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
input->actions["forward_pos"].AddPressedCallback([this]() {
|
|
if (isActive) {
|
|
wishSpeed += speedStep;
|
|
}
|
|
});
|
|
input->actions["forward_neg"].AddPressedCallback([this]() {
|
|
if (isActive) {
|
|
wishSpeed -= speedStep;
|
|
}
|
|
});
|
|
input->actions["turn_pos"].AddPressedCallback([this]() {
|
|
if (isActive) {
|
|
wishHeading = angle_normalize(wishHeading + raylib::Degree(headingStep));
|
|
}
|
|
});
|
|
input->actions["turn_neg"].AddPressedCallback([this]() {
|
|
if (isActive) {
|
|
wishHeading = angle_normalize(wishHeading - raylib::Degree(headingStep));
|
|
}
|
|
});
|
|
input->actions["space"].AddPressedCallback([this]() {
|
|
if (isActive) {
|
|
wishSpeed = 0.0f;
|
|
}
|
|
});
|
|
}
|
|
|
|
void Cleanup() override {}
|
|
|
|
void Update(float dt) override {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
// might not be efficient
|
|
auto oriented = entity->GetComponent<OrientedPhysicsComponent>();
|
|
auto tf = entity->GetComponent<TransformComponent>();
|
|
if (!tf) {
|
|
return;
|
|
}
|
|
|
|
// this is so we start with whatever velocity we had before as our wish
|
|
// heading and velocity. otherwise at the start of the game, it just
|
|
// moves you towards whatever wish was initialized with rather than
|
|
// what the actual velocity/heading was.
|
|
if (!wishInitialized) {
|
|
wishHeading = tf->get().heading;
|
|
|
|
if (auto orientedInit = entity->GetComponent<OrientedPhysicsComponent>()) {
|
|
wishSpeed = orientedInit->get().speed;
|
|
} else if (auto physInit = entity->GetComponent<PhysicsComponent>()) {
|
|
PhysicsComponent &p = physInit->get();
|
|
wishSpeed = std::sqrt(p.velocity.x * p.velocity.x + p.velocity.z * p.velocity.z);
|
|
}
|
|
|
|
wishInitialized = true;
|
|
}
|
|
|
|
if (oriented) {
|
|
OrientedPhysicsComponent &phys = oriented->get();
|
|
|
|
wishSpeed = std::clamp(wishSpeed, -phys.maxSpeed, phys.maxSpeed);
|
|
|
|
float speedDiff = wishSpeed - phys.speed;
|
|
float maxSpeedStep = phys.acceleration * dt;
|
|
phys.speed += std::clamp(speedDiff, -maxSpeedStep, maxSpeedStep);
|
|
|
|
float headingDiff = shortest_angle_delta(tf->get().heading, wishHeading);
|
|
float maxHeadingStep = phys.turningRate * dt;
|
|
float headingStepNow = std::clamp(headingDiff, -maxHeadingStep, maxHeadingStep);
|
|
tf->get().heading = angle_normalize(tf->get().heading + raylib::Degree(headingStepNow));
|
|
} else {
|
|
auto phys = entity->GetComponent<PhysicsComponent>();
|
|
if (!phys) {
|
|
return;
|
|
}
|
|
|
|
PhysicsComponent &p = phys->get();
|
|
|
|
wishSpeed = std::clamp(wishSpeed, -p.maxSpeed, p.maxSpeed);
|
|
|
|
float currentSpeed =
|
|
std::sqrt(p.velocity.x * p.velocity.x + p.velocity.z * p.velocity.z);
|
|
float speedDiff = wishSpeed - currentSpeed;
|
|
float maxSpeedStep = p.acceleration * dt;
|
|
currentSpeed += std::clamp(speedDiff, -maxSpeedStep, maxSpeedStep);
|
|
|
|
float headingDiff = shortest_angle_delta(tf->get().heading, wishHeading);
|
|
float maxHeadingStep = p.turningRate * dt;
|
|
float headingStepNow = std::clamp(headingDiff, -maxHeadingStep, maxHeadingStep);
|
|
tf->get().heading = angle_normalize(tf->get().heading + raylib::Degree(headingStepNow));
|
|
|
|
p.velocity = velocity_from_speed_heading(currentSpeed, tf->get().heading);
|
|
}
|
|
}
|
|
};
|
|
|
|
int main() {
|
|
raylib::Window window(800, 600, "CS381 - Assignment 5");
|
|
window.SetState(FLAG_WINDOW_RESIZABLE);
|
|
raylib::AudioDevice audio;
|
|
|
|
raylib::Model penguinModel("models/penguin.glb");
|
|
raylib::Model eagleModel("models/eagle.glb");
|
|
|
|
auto defaultPenguinTf = (raylib::Transform)penguinModel.GetTransform();
|
|
defaultPenguinTf = defaultPenguinTf.RotateY(raylib::Degree(90));
|
|
penguinModel.SetTransform(defaultPenguinTf);
|
|
|
|
raylib::Model ground = raylib::Mesh::Plane(10000, 10000, 50, 50, 25);
|
|
raylib::Texture snowTexture("textures/snow.jpg");
|
|
ground.GetMaterials()[0].maps[MATERIAL_MAP_DIFFUSE].texture = snowTexture;
|
|
|
|
cs381::SkyBox skybox("textures/skybox.png");
|
|
|
|
raylib::Camera3D camera({0, 400, -768}, {0, -8, 0}, {0, 1, 0}, 45.0f);
|
|
|
|
raylib::BufferedInput input;
|
|
|
|
input.actions["forward_pos"] = raylib::Action::key(KEY_W).move();
|
|
input.actions["forward_neg"] = raylib::Action::key(KEY_S).move();
|
|
input.actions["turn_pos"] = raylib::Action::key(KEY_A).move();
|
|
input.actions["turn_neg"] = raylib::Action::key(KEY_D).move();
|
|
input.actions["space"] = raylib::Action::key(KEY_SPACE).move();
|
|
input.actions["tab"] = raylib::Action::key(KEY_TAB).move();
|
|
|
|
std::vector<Entity> entities;
|
|
entities.reserve(10);
|
|
|
|
auto makePenguin = [&](float xPos, float maxSpeed, float acceleration,
|
|
float turningRate) -> Entity & {
|
|
Entity &e = entities.emplace_back();
|
|
|
|
auto &tf = e.AddComponent<TransformComponent>();
|
|
tf.position = raylib::Vector3(xPos, 0.0f, 0.0f);
|
|
tf.heading = raylib::Degree(90.0f); // face right
|
|
|
|
auto &phys = e.AddComponent<OrientedPhysicsComponent>();
|
|
phys.maxSpeed = maxSpeed;
|
|
phys.acceleration = acceleration;
|
|
phys.turningRate = turningRate;
|
|
|
|
auto &render = e.AddComponent<RenderComponent>();
|
|
render.model = &penguinModel;
|
|
|
|
auto &inp = e.AddComponent<InputComponent>();
|
|
inp.input = &input;
|
|
inp.Setup();
|
|
|
|
return e;
|
|
};
|
|
|
|
// helper to add an eagle that follows a specific penguin entity
|
|
auto makeEagle = [&](Entity &penguin, float xPos) -> Entity & {
|
|
Entity &e = entities.emplace_back();
|
|
|
|
auto &tf = e.AddComponent<TransformComponent>();
|
|
tf.position = raylib::Vector3(xPos, 200.0f, 0.0f);
|
|
tf.heading = raylib::Degree(90.0f);
|
|
|
|
auto &follow = e.AddComponent<FollowComponent>();
|
|
follow.target = &penguin;
|
|
follow.maxSpeed = 220.0f;
|
|
follow.acceleration = 70.0f;
|
|
follow.turningRate = 100.0f;
|
|
follow.hoverHeight = 200.0f;
|
|
|
|
auto &render = e.AddComponent<RenderComponent>();
|
|
render.model = &eagleModel;
|
|
|
|
auto &inp = e.AddComponent<InputComponent>();
|
|
inp.input = &input;
|
|
inp.Setup();
|
|
|
|
return e;
|
|
};
|
|
|
|
Entity &p0 = makePenguin(-400.0f, 50.0f, 50.0f, 90.0f);
|
|
Entity &p1 = makePenguin(-200.0f, 350.0f, 140.0f, 60.0f);
|
|
Entity &p2 = makePenguin(0.0f, 150.0f, 220.0f, 200.0f);
|
|
Entity &p3 = makePenguin(200.0f, 450.0f, 60.0f, 45.0f);
|
|
Entity &p4 = makePenguin(400.0f, 800.0f, 4000.0f, 500.0f);
|
|
|
|
makeEagle(p0, -400.0f);
|
|
makeEagle(p1, -200.0f);
|
|
makeEagle(p2, 0.0f);
|
|
makeEagle(p3, 200.0f);
|
|
makeEagle(p4, 400.0f);
|
|
|
|
int selectedIdx = 0;
|
|
|
|
input.actions["tab"].AddPressedCallback([&]() {
|
|
auto &old = entities[selectedIdx];
|
|
if (auto r = old.GetComponent<RenderComponent>()) {
|
|
r->get().drawBoundingBox = false;
|
|
}
|
|
if (auto i = old.GetComponent<InputComponent>()) {
|
|
i->get().isActive = false;
|
|
}
|
|
|
|
selectedIdx = (selectedIdx + 1) % (int)entities.size();
|
|
|
|
auto ¤t = entities[selectedIdx];
|
|
if (auto r = current.GetComponent<RenderComponent>()) {
|
|
r->get().drawBoundingBox = true;
|
|
}
|
|
if (auto i = current.GetComponent<InputComponent>()) {
|
|
i->get().isActive = true;
|
|
}
|
|
});
|
|
|
|
if (auto r = entities[selectedIdx].GetComponent<RenderComponent>()) {
|
|
r->get().drawBoundingBox = true;
|
|
}
|
|
|
|
if (auto i = entities[selectedIdx].GetComponent<InputComponent>()) {
|
|
i->get().isActive = true;
|
|
}
|
|
|
|
window.SetTargetFPS(60);
|
|
|
|
while (!window.ShouldClose()) {
|
|
float dt = window.GetFrameTime();
|
|
|
|
input.PollEvents();
|
|
|
|
window.BeginDrawing();
|
|
window.ClearBackground(raylib::Color::Gray());
|
|
|
|
// could probably make the camera be an entity and have a transform
|
|
// and have a camera component but nah
|
|
if (auto tf = entities[selectedIdx].GetComponent<TransformComponent>()) {
|
|
TransformComponent &actualTfForReal = tf->get();
|
|
raylib::Vector3 forward = velocity_from_speed_heading(1.0f, actualTfForReal.heading);
|
|
|
|
const float camDistance = 768.0f;
|
|
const float camHeight = 400.0f;
|
|
|
|
camera.SetPosition({actualTfForReal.position.x - forward.x * camDistance,
|
|
actualTfForReal.position.y + camHeight,
|
|
actualTfForReal.position.z - forward.z * camDistance});
|
|
|
|
camera.SetTarget({actualTfForReal.position.x, actualTfForReal.position.y - 8.0f,
|
|
actualTfForReal.position.z});
|
|
}
|
|
|
|
camera.BeginMode();
|
|
|
|
skybox.Draw();
|
|
ground.Draw({0, 0, 0}, 1.0f, raylib::Color::White());
|
|
|
|
for (Entity &e : entities) {
|
|
e.Update(dt);
|
|
}
|
|
|
|
camera.EndMode();
|
|
|
|
window.EndDrawing();
|
|
}
|
|
|
|
return 0;
|
|
}
|