Congratulations, You have successfully writen your first game.
What's Next?
Writing good code was never the aim of this book, but good code is what we all need. Lets see how we can make our code better.
What can we improve?
The list is pretty long but we will only discuss what I really hate (very much) about this code.
Let us start from our struct definition,
Intoducing a Entity Struct.
We defined two struct one to represent the Enemy
struct Enemy{
float x;
float y;
bool alive;
};
and other to represent Bullets
struct Bullet{
float x;
float y;
bool dead;
};
Look at those fields, aren't they same. Can we not define a single struct for both of them. After all they both represent a Entity in our game, then why not have a single struct for them as:
struct Entity {
float x;
float y;
bool alive;
};
Then how can we distinguish between an enemy and a bullet? Probably you could add a new field in the struct that represents the type of Entity as:
struct Entity{
//snip
std::string entity_type;
};
And then our constructor for Entity takes a string as parameter.
Entity::Entity(std::string e_type){
if (e_type == "Bullet"){
// some error handling stuff...
// create a bullet
}
// similar thing for our enemy.
}
This way we will just need to have a single Vector (Vector of Entity) instead of two and we can check for either enemy or bullet as:
for(auto &elm : vEntity){
if (elm.entity_type == "Bullet"){
// do whatever you want to do with bullet
}
// similar things ...
}
Improving the produceEnemy function
At the moment our produceEnemy
function is:
void produceEnemy() {
for (int i = 0; i < 70; ++i) {
if (i < 18)
vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + (float) i * 10 - 100, 40.0f, true});
else if (i < 36)
vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 280, 55.0f, true});
else if (i < 54)
vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 460, 75.0f, true});
else
vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 640, 95.0f, true});
}
}
Do you see all those hard-coded integers there, they are bad, very very bad. Lets see how we can improve this, the integer literals we are using in every if statements actually is ensuring that there are exactly 18 enemies per row being displayed. So what we can do is use create a new variable and use it.
int enemies_per_row = 18;
void produceEnemy(){
for(//...){
if (i < enemies_per_row){
// do sth
}
else if (i < 2 * enemies_per_row){
// do sth
}
}
}
furthermore, statements like this are very unreadable
vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + (float) i * 10 - 100, 40.0f, true});
While we create any Entity either an enemy or a bullet, we need to know the Coordinate for those entities.
So why not make our constructor take positions
and entity_type
as parameter.`
Entity::Entity(float x, float y, std::string e_type){
// create entity...
}
and insert it into the vector as,
vEntity.emplace_back(Entity(x_position, y_position, "Enemy"));
Somewhat cleaner? I hope you agree.
Cleaning up OnUserUpdate function
Everything inside a single function is a big NOOO but that is what we have done with our OnUserUpdate
function.
What we can do is, split the logic inside multiple functions, like we can add a new function that only handles user input as :
void handle_input(){
if (GetKey(olc::Key::LEFT).bHeld) {
fPlayerPositionX = fPlayerPositionX - fPlayerVel * fElapsedTime;
}
if (GetKey(olc::Key::RIGHT).bHeld) {
fPlayerPositionX = fPlayerPositionX + fPlayerVel * fElapsedTime;
}
if (GetKey(olc::Key::UP).bHeld) {
fPlayerPositionY = fPlayerPositionY - fPlayerVel * fElapsedTime;
}
if (GetKey(olc::Key::DOWN).bHeld) {
fPlayerPositionY = fPlayerPositionY + fPlayerVel * fElapsedTime;
}
if (GetKey(olc::Key::SPACE).bPressed) {
float ftempX = fPlayerPositionX;
float ftempY = fPlayerPositionY;
vBullet.emplace_back(Bullet{ftempX + float(sprPlayer->width) / 2, ftempY, false});
}
}
and then calling it inside our OnUserUpdate
function as:
bool OnUserUpdate(float fElapsedTime) override
{
// same as before
handle_input();
// same as before...
}
Similarly you can add a new function that is just dedicated for collision detection and you can just call that inside OnUserUpdate
function.
Don't repeat youself
If you didn't follow my earlier suggestion of adding a new function for collision detection then this is a must have for you.
If you look at our OnUserUpdate
function you can clearly see we are repeating ourself,
This thing is for drawing alive enemy to the screen.
for (auto elm: vEnemy) {
if (elm.alive)
DrawSprite(elm.x, elm.y, sprEnemy.get());
}
and another similar construct for moving our enemy towards the player,
for(auto &elm : vEnemy){
if(elm.alive){
float tempX = ((fPlayerPositionX + float(sprPlayer->width) / 2
) - elm.x + float(sprEnemy->height) + float(sprEnemy->width) / 2
);
float tempY = (fPlayerPositionY - elm.y + float(sprEnemy->height));
// snip
}
}
Why not merge these two into one?
for(auto &elm : vEntity){
if(elm.alive && elm.entity_type == "Enemy"){
DrawSprite(elm.x, elm.y, sprEnemy.get());
float tempX = ((fPlayerPositionX + float(sprPlayer->width) / 2
) - elm.x + float(sprEnemy->height) + float(sprEnemy->width) / 2
);
float tempY = (fPlayerPositionY - elm.y + float(sprEnemy->height));
}
}
Do you see more of these kind, just merge them.
Stay Consistent
This might sound simple and easy, but you need to be consistent with your naming convention.
- Do your variable name follow same pattern?
- Do your function name follow same pattern?
Splitting into multiple files
Do you feel like this is too much inside a single file, feel free to split the program into multiple files. Everything inside a single file is also a big no and you should try avoiding that. But it is ok for simple projects like this.
That's It
There might still be lots of things to improve but those were the things I really wanted to talk about.