#include #include #include #include #include #include #include #include #define GLM_ENABLE_EXPERIMENTAL #include #include #include #include #include #include #include #include #include #include #include #include enum KeyFrameAction { BOTTOM, TOP }; #include "gamelogic.h" #include "sceneGraph.hpp" #include struct LightSource { glm::vec3 position; glm::vec3 color; }; const glm::vec3 boxDimensions(180, 90, 90); const glm::vec3 padDimensions(30, 3, 40); const double ballRadius = 3.0; // modify to start music further into the track (seconds) const float debugStartTime = 0; SceneNode* rootNode; SceneNode* boxNode; SceneNode* ballNode; SceneNode* padNode; std::vector lightNodes; std::vector lights; glm::vec3 cameraPosition; glm::vec3 ballPosition(0, ballRadius + padDimensions.y, boxDimensions.z / 2); glm::vec3 ballDirection(1, 1, 0.2f); double padPositionX = 0; double padPositionZ = 0; unsigned int currentKeyFrame = 0; unsigned int previousKeyFrame = 0; bool hasStarted = false; bool hasLost = false; bool jumpedToNextFrame = false; bool isPaused = false; bool mouseLeftPressed = false; bool mouseLeftReleased = false; bool mouseRightPressed = false; bool mouseRightReleased = false; double totalElapsedTime = debugStartTime; double gameElapsedTime = debugStartTime; sf::SoundBuffer* buffer; sf::Sound* sound; Gloom::Shader* shader; CommandLineOptions options; double mouseSensitivity = 1.0; double lastMouseX = windowWidth / 2; double lastMouseY = windowHeight / 2; void mouseCallback(GLFWwindow* window, double x, double y) { int w, h; glfwGetWindowSize(window, &w, &h); glViewport(0, 0, w, h); double dx = x - lastMouseX; double dy = y - lastMouseY; padPositionX = glm::clamp(padPositionX - mouseSensitivity * dx / w, 0.0, 1.0); padPositionZ = glm::clamp(padPositionZ - mouseSensitivity * dy / h, 0.0, 1.0); glfwSetCursorPos(window, w / 2, h / 2); } void initGame(GLFWwindow* window, CommandLineOptions gameOptions) { options = gameOptions; buffer = new sf::SoundBuffer(); if (!buffer->loadFromFile("../res/Hall of the Mountain King.ogg")) return; glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); glfwSetCursorPosCallback(window, mouseCallback); shader = new Gloom::Shader(); shader->makeBasicShader("../res/shaders/simple.vert", "../res/shaders/simple.frag"); shader->activate(); Mesh padMesh = cube(padDimensions, glm::vec2(30, 40), true); Mesh boxMesh = cube(boxDimensions, glm::vec2(90), true, true); // inverted for interior view Mesh ballMesh = generateSphere(1.0, 40, 40); unsigned int padVAO = generateBuffer(padMesh); unsigned int boxVAO = generateBuffer(boxMesh); unsigned int ballVAO = generateBuffer(ballMesh); rootNode = createSceneNode(); boxNode = createSceneNode(); padNode = createSceneNode(); ballNode = createSceneNode(); rootNode->children.push_back(boxNode); rootNode->children.push_back(padNode); rootNode->children.push_back(ballNode); boxNode->vertexArrayObjectID = boxVAO; boxNode->VAOIndexCount = boxMesh.indices.size(); padNode->vertexArrayObjectID = padVAO; padNode->VAOIndexCount = padMesh.indices.size(); ballNode->vertexArrayObjectID = ballVAO; ballNode->VAOIndexCount = ballMesh.indices.size(); for (int i = 0; i < 3; i++) { auto* light = new SceneNode(); light->nodeType = POINT_LIGHT; light->lightIdx = i; lightNodes.push_back(light); } auto* ceilingLight = lightNodes[0]; auto* padLight = lightNodes[1]; auto* ballLight = lightNodes[2]; boxNode->children.push_back(ceilingLight); ceilingLight->position = { 0, 0, 5 }; boxNode->children.push_back(padLight); padLight->position = { -10, 0, 5 }; boxNode->children.push_back(ballLight); ballLight->position = { 10, 0, 5 }; getTimeDeltaSeconds(); // prime the timer std::cout << fmt::format("Initialized scene with {} SceneNodes.", totalChildren(rootNode)) << std::endl; std::cout << "Ready. Click to start!" << std::endl; } void updateFrame(GLFWwindow* window) { glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); double dt = getTimeDeltaSeconds(); const float cameraWallOffset = 30; const float ballBottomY = boxNode->position.y - boxDimensions.y/2 + ballRadius + padDimensions.y; const float ballTopY = boxNode->position.y + boxDimensions.y/2 - ballRadius; const float travelY = ballTopY - ballBottomY; const float ballMinX = boxNode->position.x - boxDimensions.x/2 + ballRadius; const float ballMaxX = boxNode->position.x + boxDimensions.x/2 - ballRadius; const float ballMinZ = boxNode->position.z - boxDimensions.z/2 + ballRadius; const float ballMaxZ = boxNode->position.z + boxDimensions.z/2 - ballRadius - cameraWallOffset; mouseLeftReleased = !glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) && mouseLeftPressed; mouseLeftPressed = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1); mouseRightReleased = !glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) && mouseRightPressed; mouseRightPressed = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2); if (!hasStarted) { if (mouseLeftPressed) { if (options.enableMusic) { sound = new sf::Sound(); sound->setBuffer(*buffer); sound->setPlayingOffset(sf::seconds(debugStartTime)); sound->play(); } totalElapsedTime = debugStartTime; gameElapsedTime = debugStartTime; hasStarted = true; } ballPosition.x = ballMinX + (1 - padPositionX) * (ballMaxX - ballMinX); ballPosition.y = ballBottomY; ballPosition.z = ballMinZ + (1 - padPositionZ) * (ballMaxZ + cameraWallOffset - ballMinZ); } else { totalElapsedTime += dt; if (hasLost) { if (mouseLeftReleased) { hasLost = hasStarted = false; currentKeyFrame = previousKeyFrame = 0; } } else if (isPaused) { if (mouseRightReleased) { isPaused = false; if (options.enableMusic) sound->play(); } } else { gameElapsedTime += dt; if (mouseRightReleased) { isPaused = true; if (options.enableMusic) sound->pause(); } for (unsigned int i = currentKeyFrame; i < keyFrameTimeStamps.size(); i++) { if (gameElapsedTime >= keyFrameTimeStamps[i]) currentKeyFrame = i; } jumpedToNextFrame = (currentKeyFrame != previousKeyFrame); previousKeyFrame = currentKeyFrame; double t0 = keyFrameTimeStamps[currentKeyFrame]; double t1 = keyFrameTimeStamps[currentKeyFrame + 1]; double frac = (gameElapsedTime - t0) / (t1 - t0); KeyFrameAction from = keyFrameDirections[currentKeyFrame]; KeyFrameAction to = keyFrameDirections[currentKeyFrame + 1]; double ballY = ballBottomY; if (from == BOTTOM && to == TOP) ballY = ballBottomY + travelY * frac; else if (from == TOP && to == BOTTOM) ballY = ballBottomY + travelY * (1 - frac); else if (from == TOP && to == TOP) ballY = ballTopY; const float ballSpeed = 60.0f; ballPosition.x += dt * ballSpeed * ballDirection.x; ballPosition.y = ballY; ballPosition.z += dt * ballSpeed * ballDirection.z; if (ballPosition.x < ballMinX) { ballPosition.x = ballMinX; ballDirection.x *= -1; } if (ballPosition.x > ballMaxX) { ballPosition.x = ballMaxX; ballDirection.x *= -1; } if (ballPosition.z < ballMinZ) { ballPosition.z = ballMinZ; ballDirection.z *= -1; } if (ballPosition.z > ballMaxZ) { ballPosition.z = ballMaxZ; ballDirection.z *= -1; } if (options.enableAutoplay) { padPositionX = 1 - (ballPosition.x - ballMinX) / (ballMaxX - ballMinX); padPositionZ = 1 - (ballPosition.z - ballMinZ) / (ballMaxZ + cameraWallOffset - ballMinZ); } if (jumpedToNextFrame && from == BOTTOM && to == TOP) { double padL = boxNode->position.x - boxDimensions.x/2 + (1 - padPositionX) * (boxDimensions.x - padDimensions.x); double padR = padL + padDimensions.x; double padF = boxNode->position.z - boxDimensions.z/2 + (1 - padPositionZ) * (boxDimensions.z - padDimensions.z); double padB = padF + padDimensions.z; if (ballPosition.x < padL || ballPosition.x > padR || ballPosition.z < padF || ballPosition.z > padB) { hasLost = true; if (options.enableMusic) { sound->stop(); delete sound; } } } } } cameraPosition = glm::vec3(0, 2, -20); float lookRot = -0.6f / (1 + exp(-5 * (padPositionX - 0.5))) + 0.3f; glm::mat4 view = glm::rotate(0.3f + 0.2f * float(-padPositionZ * padPositionZ), glm::vec3(1,0,0)) * glm::rotate(lookRot, glm::vec3(0,1,0)) * glm::translate(-cameraPosition); glm::mat4 proj = glm::perspective(glm::radians(80.0f), float(windowWidth) / float(windowHeight), 0.1f, 350.f); glm::mat4 VP = proj * view; boxNode->position = { 0, -10, -80 }; ballNode->position = ballPosition; ballNode->scale = glm::vec3(ballRadius); ballNode->rotation = { 0, totalElapsedTime * 2, 0 }; padNode->position = { boxNode->position.x - boxDimensions.x/2 + padDimensions.x/2 + (1 - padPositionX) * (boxDimensions.x - padDimensions.x), boxNode->position.y - boxDimensions.y/2 + padDimensions.y/2, boxNode->position.z - boxDimensions.z/2 + padDimensions.z/2 + (1 - padPositionZ) * (boxDimensions.z - padDimensions.z) }; updateNodeTransformations(rootNode, VP, glm::mat4(1.0f)); // update lights lights.clear(); for (auto* node : lightNodes) { LightSource l; l.position = node->worldPosition; switch (node->lightIdx) { case 0: l.color = glm::vec3(1, 0, 0); break; // red (ceiling) case 1: l.color = glm::vec3(0, 1, 0); break; // green (pad) case 2: l.color = glm::vec3(0, 0, 1); break; // blue (ball) } lights.push_back(l); } } void updateNodeTransformations(SceneNode* node, glm::mat4 vpSoFar, glm::mat4 modelSoFar) { glm::mat4 local = glm::translate(node->position) * glm::translate(node->referencePoint) * glm::rotate(node->rotation.y, glm::vec3(0,1,0)) * glm::rotate(node->rotation.x, glm::vec3(1,0,0)) * glm::rotate(node->rotation.z, glm::vec3(0,0,1)) * glm::scale(node->scale) * glm::translate(-node->referencePoint); node->currentTransformationMatrix = vpSoFar * local; // MVP node->modelMatrix = modelSoFar * local; // world transform node->normalMatrix = glm::transpose(glm::inverse(glm::mat3(node->modelMatrix))); if (node->nodeType == POINT_LIGHT) { node->worldPosition = glm::vec3(node->modelMatrix * glm::vec4(0, 0, 0, 1)); } for (auto* child : node->children) { updateNodeTransformations(child, node->currentTransformationMatrix, node->modelMatrix); } } void renderNode(SceneNode* node) { glUniformMatrix4fv(3, 1, GL_FALSE, glm::value_ptr(node->currentTransformationMatrix)); glUniformMatrix4fv(4, 1, GL_FALSE, glm::value_ptr(node->modelMatrix)); glUniformMatrix3fv(5, 1, GL_FALSE, glm::value_ptr(node->normalMatrix)); glUniform3fv(shader->getUniformFromName("cameraPosition"), 1, glm::value_ptr(cameraPosition)); glUniform3fv(shader->getUniformFromName("ballPosition"), 1, glm::value_ptr(ballPosition)); for (size_t i = 0; i < lights.size(); i++) { std::string base = "lights[" + std::to_string(i) + "]"; glUniform3fv(shader->getUniformFromName((base + ".position").c_str()), 1, glm::value_ptr(lights[i].position)); glUniform3fv(shader->getUniformFromName((base + ".color").c_str()), 1, glm::value_ptr(lights[i].color)); } if (node->nodeType == GEOMETRY && node->vertexArrayObjectID != -1) { glBindVertexArray(node->vertexArrayObjectID); glDrawElements(GL_TRIANGLES, node->VAOIndexCount, GL_UNSIGNED_INT, nullptr); } for (auto* child : node->children) { renderNode(child); } } void renderFrame(GLFWwindow* window) { int w, h; glfwGetWindowSize(window, &w, &h); glViewport(0, 0, w, h); renderNode(rootNode); }