PHO - Pokémon Hackers Online
Go Back   PHO - Pokémon Hackers Online > Homebrew & Coding > Coding Discussion

Coding Discussion Discuss programming and coding and get help here.

Reply
 
Thread Tools Display Modes
Old 1st March 2015, 03:37 AM   #1
Jambo51
Glory To Arstotzka
Ex-Staff
 
Jambo51's Avatar
 
Join Date: May 2012
Location: Scotland
Posts: 92
Jambo51
Default Coding a Basic Game From Scratch in C++

Hi there, guys. I've been thinking about posting this for a while now, and decided to spread the knowledge I've gained in the recent past on how to correctly build an indie game using C++ and a couple of open source libraries.

First thing's first, if you don't know basic C++ functionality, then go and learn it, I'm not about to teach you the basics of C++, nor am I about to explain the difference between objects, classes and structs, or static or member fields.

In this tutorial, it will be assumed that you understand these fundamental concepts. I will explain why I have made something static when I do so, however.

So, the very first actual coding thing we're gonna need to do is to get an IDE to develop it in. I personally use Microsoft's Visual Studio 2012 (as I got it free from University), but Eclipse CDT edition is perfectly acceptable as an alternative.

We're gonna need at least one library. Perhaps more, depending on what you decide to do with your game. This library is an open source rendering engine. As I do not (yet) have the expertise to code such an engine, and doing so is beyond the scope of this tutorial, we will rely heavily upon this library. However, we will also attempt to code the game in such a way that it could be swapped out for another library with minimal fuss.

Part 1: Getting Your Project Started
Spoiler:
OK, have you got Eclipse CDT/Visual Studio prepared?

Yes? Good. Here's what to do next depending on which IDE you're using. The process is fairly similar across the 2 IDEs, but nevertheless, here's a quick step by step on how to do it.

Visual Studio
Spoiler:
Click on File on the menu bar and select new. Then select project. Ensure you select Visual C++ from the selections and select a Win32 console application. If you cannot see C++ under the language selections, try looking for it under "Other Languages". Make sure to give your project a memorable name!

Click Next on the wizard window which pops up, then on the final window, check the "Empty Project" checkbox, then click finish.


Eclipse CDT
Spoiler:
Click on File on the menu bar and select new. Then select C++ project. On the wizard window which pops up, select "Microsoft Visual C++" in the right hand side box, labelled Toolchains, then click finish.


So, now we've got an empty project. Great. Let's start with something incredibly simple to ensure your compiler is working!


Part 2: Hello World!
Spoiler:
I can just hear you now, "Oh great, he's doing the thing that everyone does as an introduction to a language..."

Yes. Yes I am. But I'm not doing it because I think you need a lesson in how to run basic C++ applications, but because we need to ensure your compiler is working as it should!

So, create a new C++ source file and call it "Main.cpp", then populate the file with the following code:

Code:
#include <iostream>

int main()
{
    std::cout << "Hello World!\n";
    char hold;
    std::cin >> hold;
    return 0;
}
Now, compile and run this code by either hitting F5 in Visual Studio, or Ctrl-F11 in Eclipse. If it runs and opens a window displaying the text "Hello World!" (Visual Studio) or prints to the console at the bottom of the window (Eclipse) and then waits for you to enter a character, then success!


Part 3: Linking In The Irrlicht Library
Spoiler:
Now, there are two very distinct methods to include the library recommended by this tutorial.

In either case, it is recommended that you download the Irrlicht library. Because it is open source, it is possible to study the source of the engine, and even make modifications. Furthermore, since the engine only supports 32 bit precompiled library files, if you plan to make your game 64 bit, it is highly recommended that you recompile it.

Anyway, download the library (available in the link posted above) and open the zip file in your zip programme of choice. It is recommended that you use the latest available version that is not a nightly build. At time time of writing, that is 1.8.1.

Including the Library by Linking
Spoiler:

Visual Studio
Spoiler:
OK. To include the library into your project, you must right click on the project on the left or right hand side of the screen. Then select properties. The following window will pop up.


Click on VC++ Directories, and then click on "Include Directories". A small arrow should appear at the right hand side. Click the arrow, then select Edit. The following window will appear.


Click on the little folder icon and then type the following into the box that it will auto select.

Code:
$(SolutionDir)\Libs\Irrlicht\include
Now, you must repeat the procedure for the "Library Directories" section in the VC++ directories window. However, you should change the code you input to:
Code:
$(SolutionDir)\Libs\Irrlicht\lib
So far, we've only informed Visual Studio how to go about linking the library into your executable when compiling. We haven't, as you've probably noticed, actually provided the library or its headers.

So, you need to open the folder containing your project's SOLUTION file and create a new folder named "Libs" in it. Then in this folder, create a folder named "Irrlicht". Finally, you copy the folder, and contents, of the include folder over from the zip file into this Irrlicht folder. Then find the appropriate lib file in the lib folder within the zip and copy that over to a new folder named "lib" within the Irrlicht folder. By doing so, you have linked the library correctly into the executable.


Eclipse CDT
Spoiler:
Investigation required on how this operates at this stage.


Including the Library by Including the Source
Spoiler:
This is probably the easier method, albeit slower when it comes to compiling your game.

This can be achieved by copying the contents of the include and source folders in the zip file into the project's directory and then importing the existing files into your project. From this stage onwards, it should work the same either way.

This method does give a distinct advantage; You will be able to MODIFY the source of the engine should the need arise!


However, you are not finished this yet, as the executable will require the compiled Irrlicht DLL file to function. Helpfully, the coders of Irrlicht have compiled this DLL for us. it can be found in the bin folder of the zip file. Simply copy the appropriate DLL file to the directory in which your executable resides and you're good to go.

So, now we want to return to the actual project. Let's do a very quick test to check that the library has been appropriately linked in. Change Main.cpp to:

Code:
#include <iostream>
#include <irrlicht.h>

int main()
{
    irr::core::vector3df vector = irr::core::vector3df(1.0f, 2.0f, 3.0f);
    std::cout << vector.getLengthSQ();
    char hold;
    std::cin >> hold;
    return 0;
}
Now, compile and run this code by either hitting F5 in Visual Studio, or Ctrl-F11 in Eclipse.

If it runs and displays the value 14 or some variation thereof and then waits for you to enter a character, then success!


Part 4: Creating the GraphicsEngine And InputHandler Classes
Spoiler:
Now we're moving on to the real meat of the engine. This class will be designed to handle the complexities of running the 3D rendering engine and abstract it away from the game logic you will build around it.

This may sound complex, but really, it's quite easy!

So, we want to add a new class to our project and name it "GraphicsEngine". You then want to intially populate its header with the following code.

Code:
// Code for GraphicsEngine.h
#pragma once

#include <irrlicht.h>

class InputHandler;

class GraphicsEngine
{
private:
	GraphicsEngine(void);
	~GraphicsEngine(void);
	static irr::IrrlichtDevice* _device;
public:
	static bool Initialise(InputHandler* inputHandler);
	static bool Run();
	static bool Update(float deltaTime);
	static bool Render();
	static void ShutDown();
        static irr::u32 GetTime() { if (_device) { return _device->getTimer()->getRealTime(); } return 0; }
        static irr::scene::ISceneNode* AddCube(float size) { if (_device) { return _device->getSceneManager()->addCubeSceneNode(size); } return NULL; }
        static irr::scene::ISceneNode* AddSphere(float radius, int polyCount = 16) { if (_device) { return _device->getSceneManager()->addSphereSceneNode(radius, polyCount); } return NULL; }
        static irr::scene::ISceneNode* AddNodeFromMesh(irr::scene::IMesh* mesh) { if (_device) { return _device->getSceneManager()->addMeshSceneNode(mesh); } return NULL; }
        static irr::scene::ICameraSceneNode* AddCamera(const irr::core::vector3df &position = irr::core::vector3df(), const irr::core::vector3df &target = irr::core::vector3df()) { if (_device) { irr::scene::ICameraSceneNode* camera = _device->getSceneManager()->addCameraSceneNode(); camera->setPosition(position); camera->setTarget(target); return camera; } return NULL; }
};
As you can see, this class cannot be instantiated, but instead has static fields and methods. This is a cleaner method of maintaining and executing the code required.

You will also notice I have forward referenced a class named InputHandler in the header file. Doing this allows the compiler to speed up the process of compilation. Every #include slows your compilation process down!

You can also see we have included a way to get the current time from the GraphicsEngine's device and methods to add spheres, cubes and custom meshes to the world. These all return scene nodes to the calling method.

Now, go to the GraphicsEngine.cpp file and place the following code:

Code:
// Code for GraphicsEngine.cpp
#include "GraphicsEngine.h"
#include "InputHandler.h"

irr::IrrlichtDevice* GraphicsEngine::_device = NULL;


GraphicsEngine::GraphicsEngine(void)
{
}


GraphicsEngine::~GraphicsEngine(void)
{
}


bool GraphicsEngine::Initialise(InputHandler* inputHandler)
{
        irr::core::dimensions2du _dimensions = irr::core::dimensions2du(640, 480);
        if (_device)
        {
             _device->drop();
        }
	_device = irr::createDevice(irr::video::EDT_DIRECT3D9, _dimensions, 16, false, false, false, inputHandler);
	if (!_device)
	{
		std::cerr << "Failed to create device using DirectX 9, attempting Open GL..." << std::endl;
		_device = irr::createDevice(irr::video::EDT_OPENGL, _dimensions, 16, false, false, false, inputHandler);
		if (!_device)
		{
			std::cerr << "Failed to create device using Open GL, attempting BurningsVideo..." << std::endl;
			_device = irr::createDevice(irr::video::EDT_BURNINGSVIDEO, _dimensions, 16, false, false, false, inputHandler);
			if (!_device)
			{
				std::cerr << "Failed to create device using BurningsVideo, attempting software based..." << std::endl;
				_device = irr::createDevice(irr::video::EDT_SOFTWARE, _dimensions, 16, false, false, false, inputHandler);
				if (!_device)
				{
					std::cerr << "Error creating Irrlicht device" << std::endl;
					return false;
				}
			}	
		}
	}
	return true;
}

bool GraphicsEngine::Run()
{
	return _device->run();
}

bool GraphicsEngine::Update(float deltaTime)
{
        // Custom graphical effects added by you should be updated here
	return true;
}

bool GraphicsEngine::Render()
{
                if (!_device->getVideoDriver()->beginScene())
		{
			return false;
		}
		_device->getSceneManager()->drawAll();
		_device->getGUIEnvironment()->drawAll();
                // Any custom rendering software should go here.
                // EG GUI overlays
                // Placing them here ensures they render on top of the 3D models
		if (!_device->getVideoDriver()->endScene())
		{
			return false;
		}
		return true;
}

void GraphicsEngine::ShutDown()
{
    if (_device)
    {
         _device->drop();
         _device = NULL;
    }
}
At the top of the file, we include the necessary references for the Graphics engine to work. Then we have the Initialise method. This method is used to initiate the graphics engine at the startup of the game and initialise all the necessary 3D rendering software.

You will notice that the system uses a fall back setup, where attempts are made to initialise the renderer using a sequence of graphics hardware libraries. If all attempts fail, the method returns false, indicating that the game should shut down.

You will also notice, and may be perplexed by, the empty Update method. These are areas where you update the visual effects of your game once a frame and then render said effects. For example, it can be used to create a fadescreen effect.

However, you will know the game won't compile right now because the class "InputHandler" does not exist. So, add a new class and name it "InputHandler".

Now we must add specific code to make this work correctly, so look at the following code and try to understand it before you just copy it.

Code:
//Code for InputHandler.h
#pragma once

#include <irrlicht.h>

class InputHandler : public irr::IEventReceiver
{
	private:
		bool keyDown[irr::KEY_KEY_CODES_COUNT];
		bool keyWasDown[irr::KEY_KEY_CODES_COUNT];
	public:
		InputHandler();
		bool OnEvent(const irr::SEvent &incomingEvent);
		bool IsKeyDown(irr::EKEY_CODE key) const;
		bool WasKeyDown(irr::EKEY_CODE key) const;
		bool IsKeyDownButNotHeld(irr::EKEY_CODE key) const;
		bool Update();
};
This setup allows the input handler to maintain the status of the keys on the keyboard in memory for both the current frame and the previous frame. Thus allowing checks for key holds, or key presses with input being ignored until the key is released (checking for key tapping, in essence).

Next, we must set up the CPP file.

Code:
// Code for InputHandler.cpp
#pragma once

#include <irrlicht.h>
#include "InputHandler.h"

InputHandler::InputHandler()
{
	for (int i = 0; i < irr::KEY_KEY_CODES_COUNT; i++)
	{
		keyDown[i] = false;
		keyWasDown[i] = false;
	}
}

bool InputHandler::OnEvent(const irr::SEvent &incomingEvent)
{
	bool retValue = true;		
	switch (incomingEvent.EventType)
	{
		case irr::EET_KEY_INPUT_EVENT:
		{
			keyDown[incomingEvent.KeyInput.Key] = incomingEvent.KeyInput.PressedDown;
			retValue = false;
			break;
		}
	}
	return retValue;
}

bool InputHandler::IsKeyDown(irr::EKEY_CODE key) const
{
	return keyDown[key];
}

bool InputHandler::WasKeyDown(irr::EKEY_CODE key) const
{
	return keyWasDown[key];
}

bool InputHandler::IsKeyDownButNotHeld(irr::EKEY_CODE key) const
{
	return IsKeyDown(key) && !WasKeyDown(key);
}

bool InputHandler::Update()
{
	for (int i = 0; i < irr::KEY_KEY_CODES_COUNT; i++)
	{
		keyWasDown[i] = keyDown[i];
		keyDown[i] = false;
	}
	return true;
}
This code is fairly easy to understand, so I won't go into detail on the functionality right now. As things stand, we are not going to be taking mouse input into account. That will be explored later.

Now, we should compile the executable. It should compile and the very same executable should run. If it compiles, then you are good to move on to the next part.


Part 5: Using the GraphicsEngine And InputHandler Classes
Spoiler:
Right now, the Graphics Engine and Input Handler we spent so much time putting together in the last part do nothing. This is because we are not calling any of their methods or anything.

So, we go back to Main.cpp and change the code to:

Code:
// Code for Main.cpp
//
#pragma comment(lib, "Irrlicht")

#include "GraphicsEngine.h"
#include "InputHandler.h"
#include <iostream>

void CharHold()
{
        char hold;
        std::cin >> hold;
}

int main()
{
        InputHandler* ih = new InputHandler();
	if (!GraphicsEngine::Initialise(ih))
	{
                std::cout << "Graphics Engine Failed to Initialise\n";
                CharHold();
		return -1;
	}
	irr::u32 prevTime = GraphicsEngine::GetTime();
	irr::u32 currTime;
	float deltaTime;
	while (GraphicsEngine::Run())
	{
		currTime = GraphicsEngine::GetTime();
		deltaTime = (float)(currTime - prevTime) / 1000.0f;
		prevTime = currTime;
		if (!GraphicsEngine::Update(deltaTime))
		{
			break;
		}
		if (!GraphicsEngine::Render())
		{
			break;
		}
                if (ih->IsKeyDown(irr::EKEY_CODE::KEY_ESCAPE))
                {
                        break;
                }
                ih->Update();
	}
	GraphicsEngine::ShutDown();
        delete ih;
	return 0;
}
And, believe it or not, we're done for now! This programme will open a graphics window and remain open until you press the Escape key.

While nothing will render yet, rest assured that we are close to it!


Part 6: Building Pong in the Engine!
Spoiler:
So, we have our graphical output and out keyboard input. But so far, it doesn't do anything remotely interesting. Well, how about we craft the quintessential video game in it!

There's a lot of work we need to do to appropriately prepare the engine to play Pong. It may seem like overkill, but having these classes for future use will be invaluable!

So, first we need to create a base Entity class.

Code:
// Code for Entity.h

#include <irrlicht.h>

class Entity
{
    protected:
        int _id;
        irr::scene::ISceneNode* _node;
    public:
        Entity(int id = -1, const irr::core::vector3df &position = irr::core::vector3df(), const irr::core::vector3df &scale = irr::core::vector3df(1.0f, 1.0f, 1.0f));
        ~Entity();
        int GetID() const { return _id; }
        void SetID(int newID) { _id = newID; }
        irr::scene::ISceneNode* GetNode() const { return _node; }
        virtual bool Update(float deltaTime, InputHandler* ih) = 0;
};
So, I bet you have a few questions about this class. Well, let's handle them in order from top to bottom.

First, you'll have noticed I've set the encapsulated data as protected rather than private in this class. Why, you ask? Well, because it means that any derived classes have direct access to the encapsulated data, but otherwise it remains private to the host class. This will become important later.

Next, you'll have noticed I set the value of the parameters passed to the constructor in the header. What this does is set default values for these parameters. You can (and indeed should) override these values for use in your application, but this can be useful in some places.

Finally, I have set the Update function as both virtual and equal to zero. This may seem like odd behaviour since we want all entities to have an update function. However, what this ensures is that an overloading Update function must be present in any derived class and cannot rely on an implementation from the parent class. There is a variant of this which we will see later.

Now, before we go about implementing Entity.cpp, we are going to completely implement another new and important class. This class will manage any and all entities created by your game and will remove them from the simulation as and when needed.

Meet the EntityManager!

Code:
// Code for EntityManager.h
#pragma once

#include <unordered_map>
#include <irrlicht.h>

class Entity;
class InputHandler;

class EntityManager
{
private:
	static int _nextID;
	static std::unordered_map<int, Entity*>* _entities;
	EntityManager(void);
	~EntityManager(void);
public:
	static bool Initialise();
	static bool Update(float deltaTime, InputHandler* ih);
	static void ShutDown();
	static void RegisterEntity(Entity* entity);
	static void RemoveEntity(Entity* entity);
	static Entity* GetEntity(int id);
        static std::unordered_map<int, Entity*>* GetEntities() { return _entities; }
};
OK, so we know what static methods and attributes are and why we're using them, so there's nothing particularly untoward here except that this is the first use of unordered maps we've seen. If you are familiar with C# or Java, you may recognise the Dictionary or HashMap classes. This is, in effect, what an unordered map in C++ most closely resembles.

Now, let's have a look at the code for EntityManager.cpp!

Code:
//Code for EntityManager.cpp

#include "EntityManager.h"
#include "Entity.h"
#include "InputHandler.h"
#include <limits>
#include <algorithm>

int EntityManager::_nextID = 0;
std::unordered_map<int, Entity*>* EntityManager::_entities = new std::unordered_map<int, Entity*>();

EntityManager::EntityManager(void)
{
}


EntityManager::~EntityManager(void)
{
}

bool EntityManager::Initialise()
{
	for (std::unordered_map<int, Entity*>::iterator iter = _entities->begin(); iter != _entities->end(); ++iter)
	{
		if (!iter->second->Initialise())
		{
			return false;
		}
	}
	return true;
}

void EntityManager::ShutDown()
{
	for (std::unordered_map<int, Entity*>::iterator iter = _entities->begin(); iter != _entities->end(); ++iter)
	{
		delete iter->second;
		iter->second = NULL;
	}
	delete _entities;
	_entities = NULL;
}

bool EntityManager::Update(float deltaTime, InputHandler* ih)
{
	for (std::unordered_map<int, Entity*>::iterator iter = _entities->begin(); iter != _entities->end(); ++iter)
	{
		if (!iter->second->Update(deltaTime, ih))
		{
			return false;
		}
	}
	return true;
}

void EntityManager::RegisterEntity(Entity* entity)
{
	if (entity->GetID() == -1)
	{
		while (_entities->find(_nextID) != _entities->end())
		{
			++_nextID;
			if (_nextID == std::numeric_limits<int>::max())
			{
				_nextID = 0;
			}
		}
		entity->SetID(_nextID++);
	}
	else if (_entities->find(entity->GetID()) != _entities->end())
	{
		std::cerr << "Error registering entity of ID " << entity->GetID() << std::endl;
		return;
	}
	_entities[0][entity->GetID()] = entity;
}

void EntityManager::RemoveEntity(Entity* entity)
{
	std::unordered_map<int, Entity*>::iterator iter = _entities->find(entity->GetID());
	if (iter != _entities->end())
	{
		_entities->erase(iter);
	}
}

Entity* EntityManager::GetEntity(int id)
{
	std::unordered_map<int, Entity*>::iterator iter = _entities->find(id);
	if (iter != _entities->end())
	{
		return iter->second;
	}
	return NULL;
}
This is a monster of a class, designed to handle the entities you will need to create to effectively run your game! Most of the code is self explanatory, although the iterators may seem odd. However, they are not too complex, so pay them no heed!

Now we need to complete the Entity class!

Code:
//Code for Entity.cpp

#include "Entity.h"
#include "EntityManager.h"
#include "GraphicsEngine.h"

Entity::Entity(int id, const irr::core::vector3df &position, const irr::core::vector3df &scale)
{
    this->_id = id;
    this->_node = GraphicsEngine::AddCube(1.0f);
    if (this->_node)
    {
        this->_node->setScale(scale);
    }
    EntityManager::RegisterEntity(this);
}

Entity::~Entity()
{
}
And that's the Entity class' concrete implementation. As you can see, there's very little to it. That is because we rely on derived classes to add the necessary detail!

So, in the spirit of this, we are now going to extend the Entity class to create our first user controller paddle!

Code:
// Code for Paddle.h

#include "Entity.h"

class Paddle : public Entity
{
    protected:
        static irr::core::vector3df _velocity;
        Paddle(const irr::core::vector3df &position);
    public:
        Paddle();
        ~Paddle();
        virtual bool Update(float deltaTime);
        void ValidatePosition();
};
You may ask why the Update function for this class has the keyword "virtual" associated with it. This is to signify that it can be overloaded by child classes but still has a concrete implementation within this class, as opposed to the Entity parent class which has no concrete implementation.

Now we need to create the appropriate CPP file to go with this.

Code:
// Code for Paddle.cpp

#include "GraphicsEngine.h"
#include "InputHandler.h"

irr::core::vector3df Paddle::_velocity = irr::core::vector3df(0.0f, 1.0f, 0.0f);

Paddle::Paddle() : Entity(-1, irr::core::vector3df(-25.0f, 0.0f, 0.0f), ir::core::vector3df(1.0f, 4.0f, 1.0f))
{
}

Paddle::Paddle(const irr::core::vector3df &position) : Entity(-1, position, ir::core::vector3df(1.0f, 4.0f, 1.0f))
{
}

Paddle::~Paddle()
{
}

void Paddle::ValidatePosition()
{
    irr::core::vector3df pos = _node->getPosition();
    if (pos.Y < -20.0f)
    {
        _node->setPosition(pos.X, -20.0f, pos.Z);
    }
    if (pos.Y > 20.0f)
    {
        _node->setPosition(pos.X, 20.0f, pos.Z);
    }
}

bool Paddle::Update(float deltaTime, InputHandler* ih)
{
    if (ih->IsKeyDown(irr::EKEY_CODE::KEY_KEY_W))
    {
         _node->setPosition(_node->getPosition() - (_velocity * deltaTime));
    }
    else if (ih->IsKeyDown(irr::EKEY_CODE::KEY_KEY_S))
    {
         _node->setPosition(_node->getPosition() + (_velocity * deltaTime));
    }
    this->ValidatePosition();
}
}
And we have our player controller paddle! Sadly, if you were to compile and run the game right now, it would not do anything since we have not worked the new code into the old as yet.

Further, we have not added the AI controlled paddle or the ball, so we clearly still have some distance to go!

So let us start with the ball, since it is more simple and is required for the operation of the AI Paddle. For simplicity's sake, we are simply going to use a cube to represent the ball.

Code:
//Code for Ball.h
#pragma once

#include "Entity.h"

class Ball : public Entity
{
    private:
        static irr::core::vector3df _velocity;
        irr::core::vector3df _currentVelocity;
    public:
        Ball();
        ~Ball();
        bool Update(float deltaTime, InputHandler* ih);
        void ValidatePosition();
        const irr::core::vector3df & GetPosition() const { return _node->getPosition(); }
};
And now the CPP file:

Code:
// Code for Ball.cpp

#include "Ball.h"

irr::core::vector3df Ball::_velocity = irr::core::vector3df(1.0f, 1.0f, 0.0f);

Ball::Ball() : Entity(-1)
{
    switch (rand() & 3)
    {
        case 0:
            _currentVelocity = _velocity;
            break;
        case 1:
            _currentVelocity = _velocity;
            _currentVelocity.X *= -1.0f;
            break;
        case 2:
            _currentVelocity = _velocity;
            _currentVelocity.Y *= -1.0f;
            break;
        case 3:
            _currentVelocity = -1.0f * _velocity;
            break;
    }
}

Ball::~Ball()
{
}

bool Ball::Update(float deltaTime, InputHandler* ih)
{
    if (this->_node)
    {
        this->_node->setPosition(this->_node->getPosition() + (_currentVelocity * deltaTime));
    }
}

void Ball::ValidatePosition()
{
    irr::core::vector3df pos = this->_node->getPosition();
    if (pos.Y > 20.0f)
    {
        this->_node->setPosition(pos.X, 20.0f, pos.Z);
        this->_currentVelocity.Y *= -1.0f;
    }
    else if (pos.Y < -20.0f)
    {
        this->_node->setPosition(pos.X, -20.0f, pos.Z);
        this->_currentVelocity.Y *= -1.0f;
    }
    if (pos.X > 30.0f || pos.X < -30.0f)
    {
        this->_node->setPosition(0.0f, 0.0f, 0.0f);
        this->_currentVelocity *= -1.0f;
    }
}
And now our ball is ready! You may have noticed that we haven't implemented any code to deal with collision detection. Clearly this is crucial for this implementation of pong to work. However, this must wait until we have completed the AI paddle.

So that we don't have to code the paddle from scratch, we're going to extend the Paddle class and simply override the Update method!

Code:
//Code for AIPaddle.h

#pragma once

#include "Paddle.h"

class Ball;

class AIPaddle : public Paddle
{
    private:
        Ball* _ball;
    public:
        AIPaddle(Ball* ball);
        ~AIPaddle();
        bool Update(float deltaTime, InputHandler* ih);
};
And now the concrete implementation:

Code:
//Code for AIPaddle.cpp

#include "AIPaddle.h"
#include "Ball.h"

AIPaddle::AIPaddle(Ball* ball) : Paddle(irr::core::vector3df(25.0f, 0.0f, 0.0f))
{
    this->_ball = ball;
}

AIPaddle::~AIPaddle()
{
}

bool AIPaddle::Update(float deltaTime, InputHandler* ih)
{
    irr::core::vector3df ballPos = this->_ball->GetPosition();
    irr::core::vector3df myPos = this->_node->getPosition();
    if (ballPos.Y > myPos.Y)
    {
        this->_node->setPosition(myPos + (_velocity * deltaTime));
    }
    else if (ballPos.Y < myPos.Y)
    {
        this->_node->setPosition(myPos - (_velocity * deltaTime));
    }
    Paddle::ValidatePosition();
}
As you can see, we have a very basic AI built into this paddle, and it is placed at the opposite side of the screen from the player's paddle. It needs to know where the ball is in order to make its decisions, so the Ball object is passed in at construction time. It validates its position using the same validate method as the Paddle parent class.

We're not quite done, however, because we need to build collision detection into the game!

So, we need to return to the Ball class and modify the Update method to contain such hit detection.

Code:
// Code for Ball.cpp

#include "Ball.h"
#include "EntityManager.h"
#include <unordered_map>

irr::core::vector3df Ball::_velocity = irr::core::vector3df(1.0f, 1.0f, 0.0f);

Ball::Ball() : Entity(-1)
{
    switch (rand() & 3)
    {
        case 0:
            _currentVelocity = _velocity;
            break;
        case 1:
            _currentVelocity = _velocity;
            _currentVelocity.X *= -1.0f;
            break;
        case 2:
            _currentVelocity = _velocity;
            _currentVelocity.Y *= -1.0f;
            break;
        case 3:
            _currentVelocity = -1.0f * _velocity;
            break;
    }
}

Ball::~Ball()
{
}

bool Ball::Update(float deltaTime, InputHandler* ih)
{
    if (this->_node)
    {
        this->_node->setPosition(this->_node->getPosition() + (_currentVelocity * deltaTime));
        std::unordered_map<int, Entity*>* entities = EntityManager::GetEntities();
        for (auto iter = entities->begin(); iter != entities->end(); ++iter)
        {
            if (iter->first != this->_id)
            {
                if (iter->second->GetNode()->getTransformedBoundingBox().intersectsWith(this->_node->getTransformedBoundingBox()))
                {
                    _currentVelocity.X *= -1.0f;
                    this->_node->setPosition(this->_node->getPosition() + (5.0f * _currentVelocity));
                    break;
                }
            }
        }
    }
}

void Ball::ValidatePosition()
{
    irr::core::vector3df pos = this->_node->getPosition();
    if (pos.Y > 20.0f)
    {
        this->_node->setPosition(pos.X, 20.0f, pos.Z);
        this->_currentVelocity.Y *= -1.0f;
    }
    else if (pos.Y < -20.0f)
    {
        this->_node->setPosition(pos.X, -20.0f, pos.Z);
        this->_currentVelocity.Y *= -1.0f;
    }
    if (pos.X > 30.0f || pos.X < -30.0f)
    {
        this->_node->setPosition(0.0f, 0.0f, 0.0f);
        this->_currentVelocity *= -1.0f;
    }
}
And we are almost ready to go!

We've done a lot here, but as I suggested earlier, this is probably overkill for such a simple game as Pong. The point here was to develop our Entity and EntityManager classes from which everything in your game that is not static will be managed.

It's also worth noting that, in future, we will not pass the InputHandler to every entity since it is not required, but we do not yet have the required framework to remove it.

Anyway, finally, we want to actually integrate the entities into the game! We also need to create a camera to view the action. There are definitely better ways to add a camera than the way we will add one here, but once again, we lack the framework to add a camera in a better manner.

So we go back to Main.cpp and add in the new code!

Code:
// Code for Main.cpp
//
#pragma comment(lib, "Irrlicht")

#include "GraphicsEngine.h"
#include "InputHandler.h"
#include "EntityManager.h"
#include "Paddle.h"
#include "AIPaddle.h"
#include "Ball.h"
#include <iostream>

void CharHold()
{
        char hold;
        std::cin >> hold;
}

int main()
{
        InputHandler* ih = new InputHandler();
	if (!GraphicsEngine::Initialise(ih))
	{
                std::cout << "Graphics Engine Failed to Initialise\n";
                CharHold();
		return -1;
	}
        irr::scene::ICameraSceneNode* camera = GraphicsEngine::AddCamera(irr::core::vector3df(0.0f, 0.0f, -30.0f));
        new Paddle();
        new AIPaddle(new Ball());
	irr::u32 prevTime = GraphicsEngine::GetTime();
	irr::u32 currTime;
	float deltaTime;
	while (GraphicsEngine::Run())
	{
		currTime = GraphicsEngine::GetTime();
		deltaTime = (float)(currTime - prevTime) / 1000.0f;
		prevTime = currTime;
		if (!EntityManager::Update(deltaTime, ih))
		{
			break;
		}
		if (!GraphicsEngine::Update(deltaTime))
		{
			break;
		}
		if (!GraphicsEngine::Render())
		{
			break;
		}
                if (ih->IsKeyDown(irr::EKEY_CODE::KEY_ESCAPE))
                {
                        break;
                }
                ih->Update();
	}
        EntityManager::ShutDown();
	GraphicsEngine::ShutDown();
        delete ih;
	return 0;
}
As you can see, we have added the EntityManager update and shut down calls. As you will recall, we had entities automatically register with the entity manager upon creation. The shut down call of the entity manager will destroy the registered entities, so we do not need to manage them whatsoever after their instantiation.

As you can see, the Main.cpp file is beginning to become quite unwieldy. This is an issue we will address in the next section!

However, at this stage, you should have a fully functional and playable version of Pong!

Spoiler:
Extra Credit. Figure out how to track the score and put it on screen so the player knows what the score is.

You will need to research how Irrlicht handles drawing text and how to convert numbers to strings in C++ to do so. Look around on Irrlicht's Documentation and stack overflow for these!



More to follow, have to go to bed as I have work tomorrow!
__________________
I have nothing interesting to add

Last edited by Jambo51; 11th May 2015 at 12:27 AM.
Jambo51 is offline   Reply With Quote
Sponsored Links
Old 1st March 2015, 05:02 AM   #2
BlazikenXY
Pokemon Trainer and Hacker
 
BlazikenXY's Avatar
 
Join Date: Sep 2014
Location: Somewhere in world, obviously
Age: 20
Posts: 18
BlazikenXY
Default

Yay, its up!
__________________
Pokémon Ranger Academy
Status: Coming Soon ...


Need help:
Someone who has some skill for music hacking.
BlazikenXY is offline   Reply With Quote
Reply

Tags
basic, coding, game, scratch

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT. The time now is 05:34 AM.

Powered by vBulletin® Version 3.8.7
Copyright ©2000 - 2017, vBulletin Solutions, Inc. User Alert System provided by Advanced User Tagging (Lite) - vBulletin Mods & Addons Copyright © 2017 DragonByte Technologies Ltd.
Feedback Buttons provided by Advanced Post Thanks / Like (Lite) - vBulletin Mods & Addons Copyright © 2017 DragonByte Technologies Ltd.
Pokémon characters and images belong to Pokémon USA, Inc. and Nintendo.
Pokémon Hackers Online (PHO) is in no way affiliated with or endorsed by Nintendo LLC, Creatures, GAMEFREAK inc,
The Pokémon Company, Pokémon USA, Inc., The Pokémon Company International, or Wizards of the Coast.
All forum/site content (unless noted otherwise) and site designs are © 2006-2013 Pokémon Hackers Online (PHO).
Green Charizard Christos TreeckoLv100

"Black 2" by ARTPOP. Kyurem artwork by XOUS.

no new posts