Marek Kost

Game developer

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.

So this is how the final product looks

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.

Post Author: marekkost

One Reply to “Floppy bird – Flappy bird clone in Sopka engine”

Leave a Reply

Your email address will not be published. Required fields are marked *