First off, what the hell is Sopka engine? Glad you asked, it’s a custom engine I’m making in C++ with Vulkan renderer. Currently it’s very bare bones and doesn’t have a lot of functionality as I’m just reaching my first milestone after roughly 6 weeks of work.
At the moment it is just a very simple custom 3D Vulkan renderer and bunch of code around EnTT framework that makes it work. It is not very optimized as I will be working on renderer improvements next month and so I don’t think it can be used for real game yet. Flappy bird clone on the other hand, that we can handle easily.
How was this achieved? Well, to make it easier for myself I’ve put all the instantiation of objects inside my SceneSystem and coded gameplay loop inside EditorInputSystem. This allowed me to make a game relatively quickly, in just a couple of hours, with roughly 300 lines of code and that’s including comments, empty lines and duplicated code.
Now sure, I shouldn’t abuse these systems like I did but this was just to prove that one could already make a simple game inside my engine. I’m not planning on reusing any of the code from gameplay loop and actual game I will be making inside the engine will be coded very differently and making maximal use of ECS and multithreading.
Let’s look at how the code that makes it work actually looks. This is partially to remind myself of the state of the underlying code base after few weeks of work. I’m sure in couple more weeks it will be nice to look back and realize how far the engine capabilities moved since this thing was done.
Creating the scene
void SceneSystem::LoadAndSwitchToScene(entt::registry& registry) { // FIXME proper load and switch const auto newSceneEntity = registry.create(); auto& newSceneComponent = registry.assign<SceneC>(newSceneEntity); newSceneComponent.path = sceneToLoad; newSceneComponent.registry = new entt::registry; newSceneComponent.isActive = true; // debug activeScenes.push_back(newSceneComponent.registry); // debug Say::LogInfo("Scene switched:", sceneToLoad); int sceneIndex = sceneToLoad == "2" ? 2 : 1; sceneToLoad = ""; // DEBUG // create camera const auto& camera = newSceneComponent.registry->create(); auto& cameraTransform = newSceneComponent.registry->assign<TransformC>(camera); TransformUtils::CreateTransform(cameraTransform, glm::vec3(0.5f, 0.5f, 0.0f), glm::vec3(0, 90, 0)); auto& cameraComponent = newSceneComponent.registry->assign<CameraC>(camera); cameraComponent.projection = Projection::Orthographic; // begin DEBUG ECS Shader* shader = Shader::Find("uber"); const auto loadedTexture = TextureDatabase::GetTexture(AssetDatabase::GetAssetPath("square")); const auto playerMaterial = Material::CreateMaterial(shader, TextureDatabase::GetTexture(AssetDatabase::GetAssetPath("floppy"))); const auto material = Material::CreateMaterial(shader, loadedTexture); const auto floppyMesh = MeshUtils::CopyMesh(Primitives::Quad()); const auto quadMesh = MeshUtils::CopyMesh(Primitives::Quad()); // player const auto& player = newSceneComponent.registry->create(); auto& playerTransform = newSceneComponent.registry->assign<TransformC>(player); TransformUtils::CreateTransform(playerTransform, { .5f, .5f, 2.0f }); TransformUtils::Scale(playerTransform, glm::vec3(.05f, .05f, 1.f)); auto& playerMesh = newSceneComponent.registry->assign<MeshRendererC>(player); playerMesh.material = playerMaterial; playerMesh.mesh = floppyMesh; newSceneComponent.registry->assign<PlayerTag>(player); // top panel const auto& topPanel = newSceneComponent.registry->create(); auto& topPanelTransform = newSceneComponent.registry->assign<TransformC>(topPanel); TransformUtils::CreateTransform(topPanelTransform, { .5f, .975f, 2.0f }); TransformUtils::Scale(topPanelTransform, glm::vec3(1.f, .05f, 1.f)); auto& topPanelMesh = newSceneComponent.registry->assign<MeshRendererC>(topPanel); topPanelMesh.material = material; // TODO assign proper material topPanelMesh.mesh = quadMesh; newSceneComponent.registry->assign<AnimatedColorTag>(topPanel); // bottom panel const auto& bottomPanel = newSceneComponent.registry->create(); auto& bottomPanelTransform = newSceneComponent.registry->assign<TransformC>(bottomPanel); TransformUtils::CreateTransform(bottomPanelTransform, { .5f, .025f, 2.0f }); TransformUtils::Scale(bottomPanelTransform, glm::vec3(1.f, .05f, 1.f)); auto& bottomPanelMesh = newSceneComponent.registry->assign<MeshRendererC>(bottomPanel); bottomPanelMesh.material = material; // TODO assign proper material bottomPanelMesh.mesh = quadMesh; newSceneComponent.registry->assign<AnimatedColorTag>(bottomPanel); float xSpace = 0.4f; float topBorder = 0.95f; float bottomBorder = 0.05f; float playerSpace = 0.4f; std::default_random_engine generator; std::uniform_real_distribution<float> distribution(0.25f, 0.75f); for (int i = 0; i < 10; ++i) { float middleY = distribution(generator); float topObstacleScale = topBorder - (middleY + (playerSpace / 2)); float bottomObstacleScale = (middleY - (playerSpace / 2)) - bottomBorder; // top obstacle 1 const auto& topObstacle = newSceneComponent.registry->create(); auto& topObstacleTransform = newSceneComponent.registry->assign<TransformC>(topObstacle); TransformUtils::CreateTransform(topObstacleTransform, { 1.5f + xSpace * i, topBorder - topObstacleScale / 2, 2.0f }); TransformUtils::Scale(topObstacleTransform, glm::vec3(.1f, topObstacleScale, 1.f)); auto& topObstacleMesh = newSceneComponent.registry->assign<MeshRendererC>(topObstacle); topObstacleMesh.material = material; topObstacleMesh.mesh = quadMesh; newSceneComponent.registry->assign<ObstacleTag>(topObstacle); newSceneComponent.registry->assign<AnimatedColorTag>(topObstacle); // bottom obstacle 1 const auto& bottomObstacle = newSceneComponent.registry->create(); auto& bottomObstacleTransform = newSceneComponent.registry->assign<TransformC>(bottomObstacle); TransformUtils::CreateTransform(bottomObstacleTransform, { 1.5f + xSpace * i, bottomBorder + bottomObstacleScale / 2, 2.0f }); TransformUtils::Scale(bottomObstacleTransform, glm::vec3(.1f, bottomObstacleScale, 1.f)); auto& bottomObstacleMesh = newSceneComponent.registry->assign<MeshRendererC>(bottomObstacle); bottomObstacleMesh.material = material; bottomObstacleMesh.mesh = quadMesh; newSceneComponent.registry->assign<ObstacleTag>(bottomObstacle); newSceneComponent.registry->assign<AnimatedColorTag>(bottomObstacle); } }
Gameplay variables
// game variables float timer = 0; bool isPlaying = false; bool gameOver = false; glm::vec3 ogColor = {1.0f, 0.0f, 0.0f}; glm::vec3 currentColor = {1.0f, 0.0f, 0.0f}; glm::vec3 nextColor = {0.0f, 1.0f, 0.0f}; const float COLOR_CHANGE_SPEED = 0.4f; // player const float JUMP_FORCE = 2.3f; // i dunno const float GRAVITY = -6.5f; const float MAX_DOWNWARD_ACCELERATION = -1.4f; const float MAX_UPWARD_ACCELERATION = 1.1f; float forceY = MAX_DOWNWARD_ACCELERATION; // obstacles float speed = 0.2f; const float SPEED_ACCELERATION = 0.02f;
Gameplay code
oid EditorInputSystem::KeyCallback(int key, int action) { keyStates[key] = action == GLFW_PRESS ? true : action == GLFW_RELEASE ? false : true; if (keyStates[GLFW_KEY_SPACE]) { if (!isPlaying && !gameOver) { isPlaying = true; } forceY += JUMP_FORCE; } } void EditorInputSystem::MouseButtonCallback(int button, int action) { if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) { if (!isPlaying && !gameOver) { isPlaying = true; } forceY += JUMP_FORCE; } } void EditorInputSystem::CursorPositionCallback(double xpos, double ypos) { } void EditorInputSystem::Update(entt::registry& registry, float delta) { if (!isPlaying || gameOver) return; timer += delta; // move player auto view = registry.view<TransformC, MeshRendererC, PlayerTag>(); MeshRendererC* playerMesh = nullptr; for (auto& player : view) { auto& transformC = view.get<TransformC>(player); playerMesh = &view.get<MeshRendererC>(player); forceY += (forceY > 0 ? GRAVITY / 2 : GRAVITY) * delta; if(forceY < MAX_DOWNWARD_ACCELERATION) { forceY = MAX_DOWNWARD_ACCELERATION; } else if (forceY > MAX_UPWARD_ACCELERATION) { forceY = MAX_UPWARD_ACCELERATION; } TransformUtils::Translate(transformC, glm::vec3(0, forceY * delta, 0)); // HACKityhack detect collision with borders if (transformC.position.y > 0.95f - .05f / 2 || transformC.position.y < 0.05f + .05f / 2 ) { gameOver = true; Say::Log("You survived", timer, "seconds, now u go back to work."); return; } } // move obstacles speed += SPEED_ACCELERATION * delta; float highestX = 0.f; std::vector<TransformC*> transformsToReuse; auto obstacles = registry.view<TransformC, MeshRendererC, ObstacleTag>(); for(auto& obstacle : obstacles) { auto& transformC = obstacles.get<TransformC>(obstacle); auto& meshRendererC = obstacles.get<MeshRendererC>(obstacle); TransformUtils::Translate(transformC, glm::vec3(-speed * delta, 0, 0)); if (highestX < transformC.position.x) { highestX = transformC.position.x; } if (transformC.position.x < -0.2f) { transformsToReuse.push_back(&transformC); } // detect collisions glm::vec3 topLeft; glm::vec3 topRight; glm::vec3 bottomLeft; glm::vec3 bottomRight; topLeft = meshRendererC.vertices[0].pos; topRight = meshRendererC.vertices[0].pos; bottomLeft = meshRendererC.vertices[0].pos; bottomRight = meshRendererC.vertices[0].pos; for(int i = 0; i < meshRendererC.vertices.size(); ++i) { if (meshRendererC.vertices[i].pos.y >= topLeft.y && meshRendererC.vertices[i].pos.x <= topLeft.x) { topLeft = meshRendererC.vertices[i].pos; } if (meshRendererC.vertices[i].pos.y >= topRight.y && meshRendererC.vertices[i].pos.x >= topRight.x) { topRight = meshRendererC.vertices[i].pos; } if (meshRendererC.vertices[i].pos.y <= bottomLeft.y && meshRendererC.vertices[i].pos.x <= bottomLeft.x) { bottomLeft = meshRendererC.vertices[i].pos; } if (meshRendererC.vertices[i].pos.y <= bottomRight.y && meshRendererC.vertices[i].pos.x >= bottomRight.x) { bottomRight = meshRendererC.vertices[i].pos; } } for (auto& vertex : playerMesh->vertices) { if (vertex.pos.y <= topLeft.y && vertex.pos.y >= bottomRight.y && vertex.pos.x >= bottomLeft.x && vertex.pos.x <= topRight.x) { gameOver = true; Say::Log("You survived", timer, "seconds, now u go back to work."); return; } } } // reset obstacles for (auto& transform : transformsToReuse) { TransformUtils::SetPosition(*transform, glm::vec3(highestX + 0.35f, transform->position.y, transform->position.z)); } // animate color if (glm::distance(currentColor, nextColor) > 0.1f) { currentColor.r += (nextColor.r - ogColor.r) * COLOR_CHANGE_SPEED * delta; currentColor.g += (nextColor.g - ogColor.g) * COLOR_CHANGE_SPEED * delta; currentColor.b += (nextColor.b - ogColor.b) * COLOR_CHANGE_SPEED * delta; } else { auto tmpColor = ogColor; ogColor = nextColor; nextColor = tmpColor; } auto animated = registry.view<MeshRendererC, AnimatedColorTag>(); for (auto& anim : animated) { auto& vertices = animated.get<MeshRendererC>(anim).mesh->vertices; auto& vertices1 = animated.get<MeshRendererC>(anim).vertices; for (auto& v : vertices) { v.color = currentColor; } for (auto& v : vertices1) { v.color = currentColor; } } }
Special thanks to TF who challenged me to do this. I’ve been hesitant about doing such a challenge due to me not wanting to waste my precious time on something I won’t be making use of in the future. In the end, this helped me find and fix some very nasty bugs in renderer and rendering system which I would otherwise be painstakingly debugging in a moth as I’d start working on new renderer features.
How to play the “game”
To run the game, extract the 7-zip archive and run the .exe inside the Release folder.
One Reply to “Floppy bird – Flappy bird clone in Sopka engine”