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}