Skip to content
Permalink
93352eb49f
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

Session 4 - Movement

Table of Contents

  1. Using polymorphism for storing instances
  2. Basic keyboard input
  3. Updating
  4. The use of delta time
  5. Improving on keyboard input
  6. Moving with the keyboard
  7. Homework

Last session, we added inheritance and polymorphism to our project in the form of inherited classes with an abstract base class.

For this week's session, we will be looking into getting our objects to move around the scene.

Using polymorphism for storing instances

This week we will remove the suspense of how polymorphism can be used to store instances in an efficient manner!

If we used the vector container to hold our instances, it would look like this. This is because while we can see they are similar in concept, the compiler cannot, meaning we have to seperate them out. However, it stops the long amount of Draw calls by using a for loop to do the work for us.

  ...
  
std::vector<Cube> cubes;
std::vector<Sphere> spheres;
std::vector<Cone> cones;

  ...
# at some point in the code (normally the initialize part and not the draw part)

cubes.push_back(cube);
cubes.push_back(cube2);

spheres.push_back(sphere);
...

# in the draw function
	for (int i = 0; i < cubes.size(); ++i)
	{
		cubes[i].Draw();
	}

	for (int i = 0; i < spheres.size(); ++i)
	{
		spheres[i].Draw();
	}

	for (int i = 0; i < cones.size(); ++i)
	{
		cones[i].Draw();
	}

We can see that there is a lot of similar code being used in the form of the for loops, the adding of objects to the vectors and the multiple vectors. Now, imagine if we had to do this everytime we added a new class!

With polymorphism, we have the extra power of using base class pointers as pointers for the derived class. Why this is so powerful can only be shown with an example.

  ...
  
std::vector<GameObject*> objects;
  ...
# at some point in the code (normally the initialize part and not the draw part)

GameObject* cube = new Cube(glm::vec3(1, 0, 0));
GameObject* cube2 = new Cube(glm::vec3(3, 0, 1));

GameObject* sphere = new Sphere(glm::vec3(2, 0, 0));
GameObject* sphere2 = new Sphere(glm::vec3(4, 0, 1));

objects.push_back(cube);
objects.push_back(cube2);

objects.push_back(sphere);
objects.push_back(sphere2);

...

# in the draw function
	for (int i = 0; i < objects.size(); ++i)
	{
		objects[i]->Draw();
	}
  
...

# in cleanup / end of the program
 
	for (int i = 0; i < objects.size(); ++i)
	{
		delete objects[i];
	}

Note the use of GameObject* and the new keyword. We now use a vector of GameObject pointers to store any class that can derive from GameObject (in this example, Cubes and Spheres) and can go through the vector and use the correct Draw function depending on what object it is.

(How does it do this? That's a homework task!)

Make sure every new usage has a corresponding delete usage in your code!

This level of code is easier to read, is smaller and is more viable to debug. If spheres draw incorrectly but the rest are fine, we can go to the Sphere class to look.

Make sure you understand the reasoning for the above example and why we altered it. If you are unsure, just ask a member of staff about it!

Basic Keyboard Input

A static scene is boring (and also does not pass the requirements for the 217CR coursework!). Let us add basic keyboard input to make an object move.

Looking at our code, we can see two areas that work with keyboard input. These are:

void keyInput(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 27:
		exit(0);
		break;
	default:
		break;
	}
}
	glutKeyboardFunc(keyInput);

We can add some more keys for players. I will be using the following axes for movement. You can edit this to suit you.

Key What it does
w Move forward (-Z)
s Move backward (+Z)
a Move left (-X)
d Move right (+X)

Before we use them for moving, we could debug them via console printouts. Make sure to #include at the top of the main/Source.cpp.

void keyInput(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 27:
		exit(0);
		break;
	case 'w':
		std::cout << "Moving forward in - Z" << std::endl;
		break;
	default:
		break;
	}
}

I have done the first move input. Implement the rest.

Now this works, we can look into using the arrow keys.

Replace the case 'w': line with case GLUT_KEY_UP:. What happens?


Answer It worked with 'w' before but now does not work... This is because glut has two functions for keyboard input callbacks. We are using _glutKeyboardFunc(...)_ but for arrow keys and other special keys we need to use _glutSpecialFunc(...)_.

Put 'w' back in the switch statement and make another callback function for the special keys. Make sure to register the callback in the main function (like what has been done for glutKeyboardFunc. (You might have to look into what are the callback arguments for this - https://www.opengl.org/resources/libraries/glut/spec3/node54.html)

Try it yourself first and check your result against the example below.


Special Keys Example
void keySpecialInput(int key, int x, int y)
{
	switch (key)
	{
	case GLUT_KEY_UP:
		std::cout << "Moving forward in - Z" << std::endl;
		break;
	case GLUT_KEY_DOWN:
		std::cout << "Moving backward in +Z" << std::endl;
		break;
	case GLUT_KEY_LEFT:
		std::cout << "Moving left in -X" << std::endl;
		break;
	case GLUT_KEY_RIGHT:
		std::cout << "Moving right in +X" << std::endl;
		break;
	default:
		break;
	}
}
	glutSpecialFunc(keySpecialInput);

Updating

In every video game, we will want to look at each object and work out their new positions/rotations/states/reactions etc. Incrementing these by a small amount every frame gives the objects an illusion of motion. Every frame we render an image. Render enough images fast enough and the objects move.

We will want to do the same to our objects at some point, either when the player presses a key or because time has passed. (Games would be boring if the game world only updated when you pressed - Although SuperHot made that its whole concept! https://youtu.be/A1jothqmqHw?t=11)

This means we will have to add an Update function to all our objects. Thankfully because of inheritance, these changes can be done quickly. We can add a virtual function to the GameObject class and then override the definition of it in the objects that need it.

This could be either a pure virtual or just a virtual function. In this example, we will stick with a virtual function. This means that derived classes that do not have their own Update definition will use the base classes one (aka GameObject's empty one). I do this so objects that have no Update are static and do not move. If we used a pure virtual, we would have to give a definition to each class meaning each class would have to include an empty Update which is a waste of time and adds more unneeded code.

Add a virtual Update function to GameObject. Give it an empty definition in the C++ file. Then add Update to the Cube class.

Let us see Update in action! Add this code to your Cube class. It will change it's position via the Y axis, making it fall. Run the project after the change.

void Cube::Update()
{
	position.y -= 0.1f;
}

Nothing happened! Why is this? Think about it and check the answer.


Answer We never call the _Update_ function anywhere so the function (and the code inside the function) is never ran!
To fix this, we need to think about where the call could go... We have an _idle_ function that we can do this in. (Why? That's a homework task!)

Look at the answer if you have not already. How would we fix the issue? Try it yourself before you see the code answer below. Your cubes should fall (while the other objects stay still) if done right.


Code answer
	void idle()
{
	for (int i = 0; i < objects.size(); ++i)
	{
		objects[i]->Update();
	}

	glutPostRedisplay();
}

The use of delta time

This looks fine in regards to cubes falling. However there is an underlying issue which you might remember from your Unity ALL days.

At the moment, the Update happens everytime a frame is drawn. On a slow computer, fewer frames are drawn (because it takes a longer time for the hardware to create the frame) meaning fewer Update calls are done (because these only happen after the frame is drawn). On a faster computer, the opposite happens - More frames = more Update calls.

In video games this can cause a number of issues from:

Hopefully, I've proven my point!

To counter this, we need to update based on the time passed rather than the amount of frames. This means that 1 frame taking 1 second will be the same as 10 frames taking 0.1 seconds each so players should have the same experience no matter the frame rate.

The way to do this is to work out delta time, which is the amount of time that has passed between frames. We can then pass that into the Update function and use this to work out movement etc. so that it stays coherent regardless of frame rate.

Firstly, edit the Update function in both Cube and GameObject so that a float is used for an argument. Then, update the Cube Update so that the position change is multiplied by this argument. Finally, hardcode some values for the Update call in main/Source.cpp to see the changes when this value differs. (Try 0.16f, 0.33f and some more so you can see how delta time affects the movement.)

Once you have done this (or get stuck after a while), look at the completed code below.


Update with delta time
	...
	class GameObject
{
	...
	virtual void Draw() = 0;
	virtual void Update(float);
};
void GameObject::Update(float deltaTime)
{
}
	...
class Cube : public GameObject
{
	...
	void Draw();
	void Update(float);
};
void Cube::Update(float deltaTime)
{
	position.y -= 0.1f * deltaTime;
	//position.y -= 0.1f;
}
void idle()
{
	for (int i = 0; i < objects.size(); ++i)
	{
		objects[i]->Update(0.33f); # hard coded for now
	}

	glutPostRedisplay();
}

Let us now work out the actual delta time between frames and use this value for our Update function. FreeGLUT gives us a function for this via glutGet(GLUT_ELAPSED_TIME) which gives us the number of milliseconds since the first call of glutGet.

If you would rather use the C++ clock code, you can use that too - http://www.cplusplus.com/reference/ctime/clock/

Using this, we can:

  • Take a record of the last time
  • Get the new time by calling glutGet
  • Work out the delta time by subtracting the last time from the new time (giving us the milliseconds difference)
  • Dividing the delta time by 1000 to give us delta time in seconds
  • Set the last time to this new time (ready for the next frame)

Add 2 new global int variables in the main/Source.cpp called oldTimeSinceStart and newTimeSinceStart. Then copy this code into your idle function. I've added some print outs so you can see how delta time works. Once you are happy with how it works, you can remove the cout lines to stop your console getting spammed!

void idle()
{
	oldTimeSinceStart = newTimeSinceStart;
	newTimeSinceStart = glutGet(GLUT_ELAPSED_TIME);

	std::cout << " --------------------------- " << std::endl;
	std::cout << "OldTimeSinceStart: " << oldTimeSinceStart << std::endl;
	std::cout << "NewTimeSinceStart: " << newTimeSinceStart << std::endl;

	float deltaTime = (newTimeSinceStart - oldTimeSinceStart);
	std::cout << "Delta Time (ms): " << deltaTime << std::endl;
	deltaTime /= 1000.f;
	std::cout << "Delta Time (seconds): " << deltaTime << std::endl;
	std::cout << " --------------------------- " << std::endl;

	for (int i = 0; i < objects.size(); ++i)
	{
		objects[i]->Update(deltaTime);
	}

	glutPostRedisplay();
}

Improving on keyboard input

In our project, we have hardcoded our key presses in the main/Source.cpp meaning only the keys we coded in are being used. Also, objects don't have an easy way to query what keys are pressed.

We would like to be able to use all the keys on the keyboard without the pain of having to manually code a case for each one. Luckily, we can use the map data structure along with the glut callbacks to our advantage.

Don't know what the map data structure is? Time to read up! - http://www.cplusplus.com/reference/map/map/

The glut keyboard function callbacks take in either an unsigned char (for the normal keys) or an int (for the special keys) as an argument. At the moment, we take that argument and test it against our switch. What we can do is use a map to hold this key and a Boolean value (true if it is pressed and false if it is not pressed).

The keyboard map will hold:

  • char type, for the keycode - this is the key of the map
  • bool type, if pressed - this is the value of the map

The special keyboard map will hold:

  • int type, for the keycode - this is the key of the map
  • bool type, if pressed - this is the value of the map

We will change the glut keyboard function callbacks so that it sets these to true or false when a key is pressed or let go.

The magic behind this is if the key is not present in the map yet, it is like it is set to false (meaning no random keypresses appearing), and the first time we press the key, it will automatically add it in.

To sum up, the changes involve:

  • Create a map for special key presses
  • Create a map for normal key presses
  • Updating the glutKeyboardFunc function
  • Updating the glutSpecialFunc function
  • Adding the (new to you) callbacks when a key is let go (glutKeyboardUpFunc and glutSpecialUpFunc)

Creating the maps

For the maps, we will place them into the GameObject class.

"Why in that class?" you may ask. This is so we can access the keypresses directly in GameObject (or it's derived classes). In a real engine, this would be in it's own "Input" module that we would poll.

We don't want each instance of GameObject to have a copy of both maps, as this would be a waste of memory. What we can do instead is make them static variables, which means there is one copy for the class, rather than one copy per instance. Useful eh?

Create 2 static maps in the GameObject class as public variables. One that holds a <char, bool> pair (call this keys) and one that holds a <int, bool> pair (call this specialKeys). Remember to put the include in the header.

Try it yourself before you look at the code example below.


Code Example
	...
#include <map>

class GameObject
{
	...
	static std::map<char, bool> keys;
	static std::map<int, bool> specialKeys;
	...
};

As they are static variables, you will need to create the one instance the class holds. Add this to the top (after the #include) of the GameObject C++ file. Note the GameObject:: before the variable names which shows they are for the overall class and not each instance.

#include "GameObject.h"

std::map<char, bool> GameObject::keys;
std::map<int, bool> GameObject::specialKeys;

GameObject::GameObject()
	...

Updating the keyboard callback functions

Now these are in place we can update the current glut keyboard callback functions to use these maps instead of our hardcoded switches.

void keyInput(unsigned char key, int x, int y)
{
	GameObject::keys[key] = true;
	std::cout << "Key pressed: " << key << " : " << GameObject::keys[key]  << std::endl;
	//If we press escape, quit
	if (key == 27)
		exit(0);
}

Run the project and smash the keyboard a bit to see that all normal keys are being noted in the console and the values are being updated in the map! Once you are happy, you can remove the cout line if you want.

Adding new callback functions for a key up

At the moment, we can't make them false again, so the map will falsely think previously pressed keys are still being pressed! We can fix that with the following.

void keyInputUp(unsigned char key, int x, int y)
{
	GameObject::keys[key] = false;
	std::cout << "Key pressed: " << key << " : " << GameObject::keys[key] << std::endl;
}

...

# down in the main()

	glutKeyboardFunc(keyInput);
	glutKeyboardUpFunc(keyInputUp); //we have now registered our new callback function

Test this again until you are happy that the key presses and letting go of a key works! Remove the cout line if you'd rather have a littered console.

Do the same process on you own for the special key callback and add a special key up callback and register it in the main function.

Moving with the keyboard

To end with this week's tutorial, let us add the ability to move an object with the keyboard. Add this code to the Cube C++ file.

void Cube::Update(float deltaTime)
{
	if (GameObject::specialKeys[GLUT_KEY_UP] == true)
		position.z -= 1.f * deltaTime;
	if (GameObject::specialKeys[GLUT_KEY_DOWN] == true)
		position.z += 1.f * deltaTime;
}

Run the project and try the up and down arrow keys to see if they work. Once they work, add in the left and right arrow keys as well.

With this code, all instances of cube will move. How could we solve this? (Hint: Either make a new "Player" class that moves or mark instances with a Boolean that when true, allows them to move when keys are pressed, and when false, does not move them when keys are pressed.)

Homework

  • Why did I use ++i in my for loop? Look around online for the reason. How can we further improve the for loop? (Hint: it's to do with size in the loop.)
  • For virtual functions, C++ uses a vtable or "virtual table". This sometimes comes up in programming interviews so look into what one is and what it means in terms of performance.
  • Memory leaks also come up in programming interviews. Microsoft tests all games before going on the store in many ways - One of these is leaving the game on for 24 hours straight, which catches any memory leaks (as the game will crash at some point). Make sure you understand the use of new and delete along with how memory leaks work. (An interesting example - https://www.youtube.com/watch?v=86yh_VJcvX0)
  • Why do we put the Update code in the idle callback and not elsewhere? Research around to see why.
  • Think about how you could use the Update function as the integrator step of your own coursework to make objects move based on acceleration, velocity and position.
  • The new keyboard callbacks are only a few lines of code. Use lambdas instead to remove the need to create a seperate callback function to register. (We want neat code right?)