#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 #include #include #include #include #include #include #include #include #include #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); } 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> components; template T> T &AddComponent() { auto &ptr = components.emplace_back(std::make_shared()); ptr->entity = this; ptr->Setup(); return static_cast(*ptr); } template T> std::optional> GetComponent() const { for (auto &c : components) { T *cast = dynamic_cast(c.get()); if (cast) { return *cast; } } return {}; } void Update(float dt) { for (auto &c : components) { c->Update(dt); } } }; template struct Delegate {}; template struct Delegate { std::vector> functions; void connect(const std::function &func) { functions.push_back(func); } void operator+=(const std::function &func) { connect(func); } void operator()(int arg) { static_assert(std::is_same_v, "EC HW: Delegate only supports void return type"); for (const auto &func : functions) { f(std::forward(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(); 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(); 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(); 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(); auto targetT = target->GetComponent(); 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; float axisForward = 0.0f; float axisTurn = 0.0f; bool keySpace = false; void Setup() override { if (!input) { return; } input->actions["forward"].AddCallback( [this](float state, float delta) { axisForward = state; }); input->actions["turn"].AddCallback([this](float state, float delta) { axisTurn = state; }); input->actions["space"].AddCallback( [this](float state, float delta) { keySpace = (state > 0.0f); }); } void Cleanup() override {} void Update(float dt) override { if (!isActive) { return; } // might not be efficient auto oriented = entity->GetComponent(); auto tf = entity->GetComponent(); if (!tf) { return; } if (oriented) { OrientedPhysicsComponent &phys = oriented->get(); float forward = axisForward; float turn = axisTurn; phys.speed += forward * phys.acceleration * dt; tf->get().heading = angle_normalize(tf->get().heading + raylib::Degree(turn * phys.turningRate * dt)); if (keySpace) { phys.speed *= std::pow(0.05f, dt); } } else { auto phys = entity->GetComponent(); if (!phys) { return; } PhysicsComponent &p = phys->get(); float spd = std::sqrt(p.velocity.x * p.velocity.x + p.velocity.z * p.velocity.z); float forward = axisForward; float turn = axisTurn; spd += forward * p.acceleration * dt; tf->get().heading = angle_normalize(tf->get().heading + raylib::Degree(turn * p.turningRate * dt)); if (keySpace) { spd *= std::pow(0.05f, dt); } spd = std::clamp(spd, -p.maxSpeed, p.maxSpeed); p.velocity = velocity_from_speed_heading(spd, 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"] = raylib::Action::button_axis({raylib::Button::key(KEY_W)}, {raylib::Button::key(KEY_S)}) .move(); input.actions["turn"] = raylib::Action::button_axis({raylib::Button::key(KEY_A)}, {raylib::Button::key(KEY_D)}) .move(); input.actions["space"] = raylib::Action::key(KEY_SPACE).move(); input.actions["tab"] = raylib::Action::key(KEY_TAB).move(); std::vector entities; entities.reserve(10); auto makePenguin = [&](float xPos, float maxSpeed, float acceleration, float turningRate) -> Entity & { Entity &e = entities.emplace_back(); auto &tf = e.AddComponent(); tf.position = raylib::Vector3(xPos, 0.0f, 0.0f); tf.heading = raylib::Degree(90.0f); // face right auto &phys = e.AddComponent(); phys.maxSpeed = maxSpeed; phys.acceleration = acceleration; phys.turningRate = turningRate; auto &render = e.AddComponent(); render.model = &penguinModel; auto &inp = e.AddComponent(); 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(); tf.position = raylib::Vector3(xPos, 200.0f, 0.0f); tf.heading = raylib::Degree(90.0f); auto &follow = e.AddComponent(); follow.target = &penguin; follow.maxSpeed = 220.0f; follow.acceleration = 70.0f; follow.turningRate = 100.0f; follow.hoverHeight = 200.0f; auto &render = e.AddComponent(); render.model = &eagleModel; auto &inp = e.AddComponent(); 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()) { r->get().drawBoundingBox = false; } if (auto i = old.GetComponent()) { i->get().isActive = false; } selectedIdx = (selectedIdx + 1) % (int)entities.size(); auto ¤t = entities[selectedIdx]; if (auto r = current.GetComponent()) { r->get().drawBoundingBox = true; } if (auto i = current.GetComponent()) { i->get().isActive = true; } }); if (auto r = entities[selectedIdx].GetComponent()) { r->get().drawBoundingBox = true; } if (auto i = entities[selectedIdx].GetComponent()) { 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 &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; }