Boulder Crash


A Crash Bandicoot boulder level clone

This project uses a custom engine written in C++ and DirectX. The engine features post processing effects, hardware skinning, particle effects and shadow mapping. The game is a copy of the boulder levels from Crash Bandicoot. The level and gates were modelled by me, the other models were found online. All animations come from Mixamo. The engine doesn’t feature an editor so designing the level was partially done in code and mostly created in 3D studio max and exported through max script to the engine.

Trailer

Take a look at the trailer for the game below! A longer gameplay video can be found here

About the project

This project uses an extended verions of a basic C++ engine created during a university course, I added shadowmapping to the engine in about four days. API’s used in this engine are DirectX, PhysX and Fmod.

It includes a main menu, a controls menu, the main level, a pause menu with reset, resume and exit options and an end game screen that displays your final score upon death or victory. It has full controller support and has basic ambient sounds and music.

The player controls a fox character that can run, jump and attack. The goal is to get to the end of the level without dying to the boulder that is chasing you down. Not only do you have to get to the end of the level, you also have to get as many points as you can by breaking crates and collecting apples while you are being chased by the boulder. The level features a lot of obstacles the player has to avoid or jump over. This makes it so you sometimes have to choose between getting crates or safely jumping over the pits.

As the engine does not include any level editor, the whole level was first modeled in 3DS max and then exported to the Overlord Engine. This was done using a combination of MaxScript to write the position, rotation and the scale for all items in the scene to a csv file. This csv file is then used to load the correct models in the engine and automatically have them in the right position. How this is done in code is outlined in the next section.

The main level model and the gate model were both modeled and textured by me, the other models (character, trees, plants, apples…) were found online. The animations for the character are from Mixamo, imported to the engine’s own animation format.

Code snippets

Shadow Mapping

This first code example shows how shadow mapping is done in the engine. The most important functions are the Begin, Draw and End functions. These are the hearth of the shadow mapping process. In the begin stage, the main render targets and shader resources get unbound and the shadow map render target gets bound. The Draw function gets called for every object in the scene, here the scene gets rendered from the POV of the light. In the End function, the render targets get unbound again so the produced shadow map can be used in the final scene draw.

  1#include "stdafx.h"
  2#include "ShadowMapRenderer.h"
  3#include "stdafx.h"
  4#include "RenderTarget.h"
  5#include "ShadowMapMaterial.h"
  6#include "../../../Graphics/MeshFilter.h"
  7#include "OverlordGame.h"
  8#include "../../../Content/ContentManager.h"
  9#include "../../../Scenegraph/GameObject.h"
 10#include "../../../Scenegraph/GameScene.h"
 11#include "../../../Scenegraph/SceneManager.h"
 12#include "../../../Components/ModelComponent.h"
 13#include "ModelAnimator.h"
 14#include "../../../Components/TransformComponent.h"
 15
 16ShadowMapRenderer::ShadowMapRenderer()
 17	:m_Size(850.0f)
 18{
 19}
 20
 21ShadowMapRenderer::~ShadowMapRenderer()
 22{
 23	SafeDelete(m_pShadowRT);
 24	SafeDelete(m_pShadowMat);
 25}
 26
 27void ShadowMapRenderer::Initialize(const GameContext & gameContext)
 28{
 29	//Create shadow Render Target
 30	m_pShadowRT = new RenderTarget(gameContext.pDevice);
 31	auto windowSettings = OverlordGame::GetGameSettings().Window;
 32
 33	RENDERTARGET_DESC desc;
 34	//desc.pDepth = true;
 35	desc.EnableDepthBuffer = true;
 36	desc.EnableDepthSRV = true;
 37	desc.EnableColorBuffer = false;
 38	desc.Width = windowSettings.Width;
 39	desc.Height = windowSettings.Height;
 40
 41	m_pShadowRT->Create(desc);
 42
 43	//Create ShadowMaterial
 44	m_pShadowMat = new ShadowMapMaterial();
 45	m_pShadowMat->Initialize(gameContext);
 46
 47	m_IsInitialized = true;
 48}
 49
 50void ShadowMapRenderer::SetLight(XMFLOAT3 position, XMFLOAT3 direction)
 51{
 52	m_LightPosition = position;
 53	m_LightDirection = direction;
 54
 55	auto windowSettings = OverlordGame::GetGameSettings().Window;
 56	float viewWidth = (m_Size>0) ? m_Size * windowSettings.AspectRatio : windowSettings.Width;
 57	float viewHeight = (m_Size>0) ? m_Size : windowSettings.Height;
 58	auto projection = XMMatrixOrthographicLH(viewWidth, viewHeight,10.0f, 2500.0f);
 59
 60	XMVECTOR upVec = { 0,1,0 };
 61	XMVECTOR positionVec = XMLoadFloat3(&m_LightPosition);
 62	XMVECTOR directionVec = XMLoadFloat3(&m_LightDirection);
 63
 64	auto view = XMMatrixLookAtLH(positionVec, positionVec + directionVec, upVec);
 65
 66	XMMATRIX result = view * projection;
 67	XMStoreFloat4x4(&m_LightVP, result);
 68}
 69
 70void ShadowMapRenderer::SetLightPosition(XMFLOAT3 position)
 71{
 72	SetLight(position, m_LightDirection);
 73}
 74
 75void ShadowMapRenderer::SetLightDirection(XMFLOAT3 direction)
 76{
 77	SetLight(m_LightPosition, direction);
 78}
 79
 80void ShadowMapRenderer::Begin(const GameContext & gameContext)
 81{
 82	m_pOldRT = SceneManager::GetInstance()->GetGame()->GetRenderTarget();
 83
 84	// Unbind shader resources
 85	ID3D11ShaderResourceView* nullViews[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
 86	gameContext.pDeviceContext->PSSetShaderResources(0, 8, nullViews);
 87
 88	// Unbind render targets
 89	auto depth = m_pShadowRT->GetDepthStencilView();
 90	gameContext.pDeviceContext->OMSetRenderTargets(0, NULL, depth);
 91	gameContext.pDeviceContext->ClearDepthStencilView(m_pShadowRT->GetDepthStencilView(), D3D11_CLEAR_DEPTH,1.0f, 0);
 92}
 93
 94void ShadowMapRenderer::Draw(const GameContext & gameContext, ModelComponent * pModelComponent, XMFLOAT4X4 world)
 95{
 96	auto pMeshFilter = pModelComponent->GetMeshFilter();
 97	auto pAnimator = pModelComponent->GetAnimator();
 98
 99	if (!pMeshFilter->m_ShadowEnabled)
100		return;
101
102	//If the model has an animator, set the corresponding booleans in the shadowMaterial
103	if (pAnimator)
104	{
105		m_pShadowMat->SetHasAnimations(true);
106		m_pShadowMat->SetBones(pAnimator->GetBoneTransforms());
107	}
108	else
109		m_pShadowMat->SetHasAnimations(false);
110
111	m_pShadowMat->SetLightVP(m_LightVP);
112	m_pShadowMat->SetWorldVP(world);
113
114	//Set Inputlayout
115	gameContext.pDeviceContext->IASetInputLayout(m_pShadowMat->GetInputLayout());
116
117	//Set Vertex Buffer
118	UINT offset = 0;
119	auto vertexBufferData = pMeshFilter->GetVertexBufferData(gameContext, m_pShadowMat);
120	gameContext.pDeviceContext->IASetVertexBuffers(0, 1, &vertexBufferData.pVertexBuffer, &vertexBufferData.VertexStride, &offset);
121
122	//Set Index Buffer
123	gameContext.pDeviceContext->IASetIndexBuffer(pMeshFilter->m_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);
124
125	//Set Primitive Topology
126	gameContext.pDeviceContext->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
127
128	//DRAW
129	auto tech = m_pShadowMat->GetTechnique();
130	D3DX11_TECHNIQUE_DESC techDesc;
131	tech->GetDesc(&techDesc);
132	for (UINT p = 0; p < techDesc.Passes; ++p)
133	{
134		tech->GetPassByIndex(p)->Apply(0, gameContext.pDeviceContext);
135		gameContext.pDeviceContext->DrawIndexed(pMeshFilter->m_IndexCount, 0, 0);
136	}
137}
138
139void ShadowMapRenderer::End(const GameContext & gameContext)
140{
141	// Unbind render targets
142	ID3D11RenderTargetView* oldView = m_pOldRT->GetRenderTargetView();
143	gameContext.pDeviceContext->OMSetRenderTargets(1, &oldView, m_pOldRT->GetDepthStencilView());
144	gameContext.pDeviceContext->ClearRenderTargetView(m_pOldRT->GetRenderTargetView(), reinterpret_cast<const float*>(&Colors::Black));
145	gameContext.pDeviceContext->ClearDepthStencilView(m_pOldRT->GetDepthStencilView(), D3D10_CLEAR_DEPTH, 1.0f, 0);
146}
147
148XMFLOAT3 ShadowMapRenderer::GetLightDirection()
149{
150	return m_LightDirection;
151}
152
153XMFLOAT4X4 ShadowMapRenderer::GetLightVP()
154{
155	return m_LightVP;
156}
157
158ShadowMapMaterial * ShadowMapRenderer::GetMaterial()
159{
160	return m_pShadowMat;
161}
162
163ID3D11ShaderResourceView * ShadowMapRenderer::GetShadowMap()
164{
165	return m_pShadowRT->GetDepthShad
166}

Static Loader

The following code snippet contains the code that loads static objects into the game. It uses a .csv file - generated from 3DS Max scene using MaxScript - that contains the position, rotation and scale of the items and some extra information to know which item has to be loaded into the game. Using this technique I was able to create a sizeable level with ease, not having to hard code anything into the level.

The loader is a templated header only class and uses a csv reader header include to parse the data. The constructor requires the file path to the csv file that has to be loaded, as well as a pointer to the scene the item has to be added to.

This information is used in the actual load function, which reads the raw data from the csv file and uses it to construct items from the templated class and position and scale them correctly. Once the object is loaded and positioned, it is added to the scene specified in the constructor of the loader. From this point on the scene is responsible of managing the loaded items.

 1#pragma once
 2#include "CrashBandicoot/Scene/CrashScene.h"
 3#include "Scenegraph/GameObject.h"
 4#include "Components/Components.h"
 5#include "csv.h"
 6
 7template <class T>
 8class StaticObjectLoader
 9{
10public:
11	StaticObjectLoader(const std::string& filePath, CrashScene* scene);
12	~StaticObjectLoader() = default;
13
14	void LoadObjects();
15private:
16	std::string m_FilePath;
17	CrashScene* m_pScene;
18
19	// -------------------------
20	// Disabling default copy constructor and default
21	// assignment operator.
22	// -------------------------
23	StaticObjectLoader(const StaticObjectLoader& obj) = delete;
24	StaticObjectLoader& operator=(const StaticObjectLoader& obj) = delete;
25};
26
27template<class T>
28inline StaticObjectLoader<T>::StaticObjectLoader(const std::string& filePath, CrashScene * scene)
29	:m_FilePath(filePath) ,m_pScene(scene)
30{
31}
32
33template<class T>
34inline void StaticObjectLoader<T>::LoadObjects()
35{
36	io::CSVReader<7> in(m_FilePath);
37	in.read_header(io::ignore_extra_column, "OvlX", "OvlY", "OvlZ", "OvlRotX", "OvlRotY", "OvlRotZ", "Idx");
38	double posX, posY, posZ, rotX, rotY, rotZ;
39	int idx;
40
41	while(in.read_row(posX, posY, posZ, rotX, rotY, rotZ, idx))
42	{
43		T* object = new T(idx);
44		static_cast<GameObject*>(object)->GetTransform()->Translate(float(posX), float(posY), float(posZ));
45		static_cast<GameObject*>(object)->GetTransform()->Rotate(float(rotX), float(rotY), float(rotZ), false);
46		m_pScene->AddChild(object);
47	}
48}

Obstacle

The following snippet is an example of how interactive objects work. The initialize and post initialize functions set all the basic parameters for the object like the material, collision boxes, collision groups, trigger and more. It is also an indication of the workflow used in the engine as well as the general setup.

  1#include "stdafx.h"
  2#include "BarrierPost.h"
  3#include "Components/Components.h"
  4#include "CrashBandicoot/Scene/CrashScene.h"
  5#include "Physx/PhysxManager.h"
  6#include "Boulder.h"
  7#include "Materials/Shadow/DiffuseMaterial_Shadow.h"
  8
  9int BarrierPost::MaterialIdx = -1;
 10
 11BarrierPost::BarrierPost(const XMFLOAT3& initPos, const XMFLOAT3& initRot)
 12	:m_InitPos(initPos), m_InitRot(initRot), m_CurrRot(initRot)
 13{
 14}
 15
 16BarrierPost::~BarrierPost()
 17{
 18}
 19
 20void BarrierPost::KnockOver()
 21{
 22	if(!m_IsKnocked)
 23	{
 24		m_IsKnocked = true;
 25		m_FinalRot = { m_InitRot.x - 80.0f, m_InitRot.y, m_InitRot.z };
 26	}
 27}
 28
 29void BarrierPost::Initialize(const GameContext & gameContext)
 30{
 31	UNREFERENCED_PARAMETER(gameContext);
 32
 33	if(MaterialIdx == -1)
 34	{
 35		auto mat = new DiffuseMaterial_Shadow();
 36		mat->SetDiffuseTexture(L"./Resources/Textures/CrashBandicoot/BarrierWood.png");
 37		mat->SetLightDirection(gameContext.pShadowMapper->GetLightDirection());
 38		MaterialIdx = gameContext.pMaterialManager->AddNextMaterial(mat);
 39	}
 40	//Model Component
 41	auto model = new ModelComponent(L"./Resources/Meshes/CrashBandicoot/BarrierPost.ovm");
 42	model->SetMaterial(MaterialIdx);
 43	AddComponent(model);
 44
 45	auto physX = PhysxManager::GetInstance()->GetPhysics();
 46
 47	auto nothing = physX->createMaterial(0.0f, 0.0f, 0);
 48
 49	//Rigid Body used for collsion
 50	auto collObj = new GameObject();
 51	collObj->GetTransform()->Translate(m_InitPos);
 52	collObj->GetTransform()->Rotate(m_InitRot);
 53	auto rigidColl = new RigidBodyComponent(true);
 54	rigidColl->SetCollisionGroup(CollisionGroupFlag(CollisionChanel::Character));
 55	rigidColl->SetCollisionIgnoreGroups(CollisionGroupFlag(CollisionChanel::Boulder));
 56	collObj->AddComponent(rigidColl);
 57
 58	std::shared_ptr<PxGeometry> geom = std::make_shared<PxBoxGeometry>(10.8f, 11.0f, 1.05f);
 59	auto coll = new ColliderComponent(geom, *nothing, PxTransform(0, 11.0f,0));
 60	collObj->AddComponent(coll);
 61	AddChild(collObj);
 62
 63	//Add child obj for trigger
 64	auto triggerObj = new GameObject();
 65	triggerObj->GetTransform()->Translate(m_InitPos);
 66	triggerObj->GetTransform()->Rotate(m_InitRot);
 67	auto triggerRigid = new RigidBodyComponent();
 68	triggerRigid->SetKinematic(true);
 69	triggerRigid->SetCollisionGroup(CollisionGroupFlag(CollisionChanel::Boulder));
 70
 71	triggerObj->AddComponent(triggerRigid);
 72	geom = std::make_shared<PxBoxGeometry>(12.8f, 13.0f, 1.55f);
 73	auto triggerColl = new ColliderComponent(geom, *nothing, PxTransform(0, 12.0f, 0));
 74	triggerColl->EnableTrigger(true);
 75	triggerObj->AddComponent(triggerColl);
 76
 77	triggerObj->SetOnTriggerCallBack(OnBarrierPostTrigger);
 78
 79	AddChild(triggerObj);
 80}
 81
 82void BarrierPost::PostInitialize(const GameContext & gameContext)
 83{
 84	UNREFERENCED_PARAMETER(gameContext);
 85	GetTransform()->Translate(m_InitPos);
 86	GetTransform()->Rotate(m_InitRot);
 87}
 88
 89void BarrierPost::Draw(const GameContext & gameContext)
 90{
 91	UNREFERENCED_PARAMETER(gameContext);
 92}
 93
 94void BarrierPost::Update(const GameContext & gameContext)
 95{
 96	if(m_IsKnocked && !m_ReachedRot)
 97	{
 98		auto newRot = XMFLOAT3{ m_CurrRot.x - m_RotationSpeed * gameContext.pGameTime->GetElapsed(), m_CurrRot.y, m_CurrRot.z };
 99
100		if(newRot.x < m_FinalRot.x)
101		{
102			newRot.x = m_FinalRot.x;
103			m_ReachedRot = true;
104		}
105		GetTransform()->Rotate(newRot);
106		m_CurrRot = newRot;
107	}
108}
109
110void BarrierPost::OnBarrierPostTrigger(GameObject * triggerObject, GameObject * otherObject, TriggerAction action)
111{
112	if(action == ENTER)
113	{
114		auto barrier = static_cast<BarrierPost*>(triggerObject->GetParent());
115		auto boulder = dynamic_cast<Boulder*>(otherObject);
116		if (boulder && barrier)
117		{
118			barrier->KnockOver();
119		}
120	}
121}