I am going to walk you through the creation of a Megaman-inspired shooter/platformer game. We will be more focused on the shooting aspects of the gameplay rather than the platforming. In this tutorial I will be using Construct 2 as the tool to make the game, but I will explain the logic using pseudocode so that you can follow this tutorial in any language or engine of your choice.
In order to focus on the gameplay implementation, I won't explain every Construct 2 feature; I am going to assume that you know the basics such as loading a sprite, basic collision, or playing sounds. With that said, let's start making the game.
Prepare the Artwork
First things first, we need to have sprites for our game. Thankfully, opengameart has us covered with their wonderful collection of legal game art. We need four sets of sprites; one hero, one enemy, and tiles for platforms.
-
Hero (by Redshrike):
http://opengameart.org/content/xeon-ultimate-smash-friends -
Enemy (also by Redshrike):
http://opengameart.org/content/fighting-robot-for-ultimate-smash-friends -
Tiles (by robotality):
http://opengameart.org/content/prototyping-2d-pixelart-tilesets
To use them in Construct 2, I crop the hero's sprites into individual frames using GIMP.
Basic Movement
I will be using Construct 2's platform behaviour for the rest of the tutorial so that I can focus on the shooting and AI part of the game, which was the main focus of this tutorial.
Basic Movement Explained
If you are working in another language, or want to implement your own basic platformer movement instead of using the built-in behaviour. You only need to use the code in this section if you aren't going to use Construct 2's built-in behavior.
To begin, we need to consider three ways our hero can move; walking right, walking left, or jumping. Each frame, we update the game simulation.
number moveSpeed = 50; function update() { moveHero(); }
To update the player's character, we implement basic movement like this:
function moveHero() { // player is pressing this key down if (keyDown(KEY_LEFT)) { hero.x -= moveSpeed * deltaTime; hero.animate("walkLeft"); } if (keyDown(KEY_RIGHT)) { hero.x += moveSpeed * deltaTime; hero.animate("walkRight"); } if (keyDown(KEY_UP)) { hero.jump(); hero.animate("jump"); } // a key that was just unpressed if (keyReleased(KEY_LEFT)) { hero.animate("standLeft"); } if (keyReleased(KEY_RIGHT)) { hero.animate("standRight"); } }
I use another function to do jumping because jumping isn't just a matter of changing the y value but also calculating the gravity. We will also have a function that listens whether or not a key has just been released, to return our hero animation to a standing animation.
Let's talk about how to make the player jump. The hero will need to know whether he is currently jumping or not, and also whether he is currently falling or not. So we'll declare two new variables: isJumping and isFalling. By default, both are false, which means the hero is standing on a platform.
To perform a jump we must first check whether or not both values are false, and then make the isJump true.
Function jump() { if (!isJumping && !isFalling) { isJumping = True } }
For the hero to be able to jump, we need a variable called jumpPower and gravity. The jumpPower's default value is -20 and gravity is 1. The logic is to add the value of jump power to hero's Y position and to add gravity to jump power's value.
We do this every tick. Maybe this isn't the most realistic gravity physics there is, but games don't need to be realistic, they just need to be believable, that's why some games have super human jump and double jump. The code below belongs in the update function.
If (isJumping || isFalling) { hero.y += hero.jumpPower; hero.jumpPower += hero.gravity; } // eventually jump power will be greater than zero, and that means the hero is falling if (hero.jumpPower >= 0) { isJumping = False; isFalling = True; } // to make a stop to the fall, do something when the hero overlaps the platform if (hero.isOverlapping(platform1)) { // platform1 is the platform that our hero can step on // resets the variable to their default value isJumping = False; isFalling = False; hero.jumpPower = -20; } // and then there's the free fall, when player falls over the edge of a platform if (!hero.isOverlapping(platform1) && hero.jumpPower < 0 && !isJumping) { // !hero.isOverlapping(platform1) checks whether or not our hero is standing on a platform // and if jumpPower is less than zero and the player is not currently jumping, then that means // he's falling // setting these two values like this will make the player fall. hero.jumpPower = 0; isFalling = true; }
Construct 2's built-in platform behaviour replicates the above example code, which is given only the help those working in another language.
Implementing the Shooting
Now comes the shooting part of the game. In the Megaman series there are three types of shots: normal shots, charged shots, and boss energy shots.
Normal shots are self explanatory. Charged shots are shots that are charged first before released, these charged shots come in two types: half charged, and fully charged. These charged attacks are stronger than normal shots, with the fully charged become the strongest.
Boss energy shots are shots with power that the player acquired after defeating each bosses. The damage is the same as normal but they have special properties that normal shots don't have.
Now that we know each shot's type, let's begin to make them. First let's see the logic behind how we use each shot. Here we assume that the Z button on the keyboard is used to fire a shot. We'll implement two different behaviors:
- Normal shots: the player presses z and then immediately releases it (tap the button). The bullet will be shot once each tap. The animation will change to shoot animation before immediately switching to the standing animation.
- Charged shots: the player presses Z. The first normal bullet will be shot. The animation will change to shoot before immediately switching to the standing animation. If Z continues to be pressed then a charging effect will be added on top of the playing animation (standing, walking). If the Z button is released in less than 5 seconds since first charging, then a half-charged bullet will be shot. If the Z button is released after 5 seconds, a fully charged bullet will be shot.
- Boss energy shots: our hero must first equip the bullet he acquired after defeating a boss. After equipping, the player will press another button to shot this bullet. This bullet behaviour varies, and needs to be uniquely coded for every bullet.
Now, let's start to code. Because our hero can shoot left and right we need to know which direction he's currently facing. Let's declare a new variable called facing that stores a string value of whether the hero is facing left or right.
String facing = "right"; // which direction the hero is currently facing function moveHero() { // player is pressing this key down if (keyDown(KEY_LEFT)) { hero.x -= moveSpeed * deltaTime; hero.animate("walkLeft"); facing = "left"; } if (keyDown(KEY_RIGHT)) { hero.x += moveSpeed * deltaTime; hero.animate("walkRight"); facing = "right"; } // ... the continuation of moveHero() function goes here } function update() { // ...the update code that we previously wrote goes here... // player press this key once if (keyPressed(KEY_Z)) { if (facing == "right") { hero.animate("Shoot"); // we will add shooting function here later } else if (facing == "left") { hero.animate("Shoot"); hero.mirrorSprite(); // this function flips the sprite horizontally } } if (keyReleased(KEY_Z)) { if (facing == "right") { hero.animate("standRight"); } else if (facing == "left") { hero.animate("standLeft"); hero.mirrorSprite(); // we need to call this again because the sprite was mirrored // if we don't mirror the sprite again, standLeft will look like standRight } } }
Before we shoot a bullet, we need to look at the properties the bullet has:
- Power: attack power of the bullet, the damage it will dealt to the enemy
- Speed: how fast the bullet goes
- Angle: the shooting angle, determines at which direction the bullet goes.
These properties will differ for each bullet. In particular, the power property will be different. The angle property is normally only one of two values; whether the bullet is shot right or left, unless it's a boss energy bullet that may shot at a unique angle.
Shot variations will be discussed later so now I will only cover basic shots. The following is the piece of code that shoots a bullet.
// first, we create a function that creates a new bullet Function shoot(string pathToSprite, number bulletPower, number bulletSpeed, number bulletAngle) { myBullet = new Bullet(pathToSprite); myBullet.power = bulletPower; // the bullet class or object has two private variables that moves it according to its angle // more explanation to these two lines need more math, so I choose not to explain // I assume your engine of choice have a way to move an object according to its angle ySpeed = Math.sin(bulletAngle) * bulletSpeed; xSpeed = Math.cos(bulletAngle) * bulletSpeed; } // this is Bullet class' function that's called every frame, this moves the bullet according to its angle function moveBullet() { x += xSpeed * deltaTime; y += ySpeed * deltaTime; } // and this is the modification to our previous update() function function update() { // ...the update code that we previously wrote goes here... // player press this key once if (keyPressed(KEY_Z)) { if (facing == "right") { hero.animate("Shoot"); hero.shoot("path/to/sprite.png", 10, 400, 0); } else if (facing == "left") { hero.animate("Shoot"); hero.mirrorSprite(); // this function flips the sprite horizontally hero.shoot("path/to/sprite.png", 10, 400, 180); // the angle is 180 so that the bullet goes left } } // .. the continuation of update code goes here... }
Charged Shots
Some bullets can be more powerful than others. To create a charged shot we need a variable named chargedTime, which will increment each second the player holds Z down, and will return to zero when the bullet is fired. The changes to the update code are as follows:
// player just released z key if (keyReleased(KEY_Z)) { if (chargedTime > 0 && chargedTime <= 5) { if (facing == "right") { hero.animate("Shoot"); hero.shoot("path/to/halfChargedBullet.png", 20, 400, 0); chargedTime = 0; } else if (facing == "left") { hero.animate("Shoot"); hero.mirrorSprite(); // this function flips the sprite horizontally hero.shoot("path/to/halfChargedBullet.png", 20, 400, 180); chargedTime = 0; } } else if (chargedTime > 5) { if (facing == "right") { hero.animate("Shoot"); hero.shoot("path/to/fullChargedBullet.png", 40, 400, 0); chargedTime = 0; } else if (facing == "left") { hero.animate("Shoot"); hero.mirrorSprite(); // this function flips the sprite horizontally hero.shoot("path/to/fullChargedBullet.png", 40, 400, 180); chargedTime = 0; } } if (facing == "right") { hero.animate("standRight"); } else if (facing == "left") { hero.animate("standLeft"); hero.mirrorSprite(); // we need to call this again because the sprite was mirrored // if we don't mirror the sprite again, standLeft will look like standRight } } // player is pressing this key down if (keyDown(KEY_Z)) { // this is the function that adds the value of chargedTime every second // this keyDown block of code will be run every frame, which is less than a second // your engine of choice should have a way to tell whether a second has passed or not addChargedTime(); }
Our hero character new moves left, right, and jumps according to our input, and also shoots bullets, whether normal, half charged, or fully charged.
Implementing Enemies
We now have a controllable hero. Let's call it Xeon for simplicity's sake. He can perform some basic movements like walking, jumping, and shooting. That's great! But what good is the ability to shoot without something to shoot at, right? That's why this time we're going to make our first enemy.
Let's design our enemy's attributes before we start to code it.
Health: How many healths our enemy have determines how many shots (and what kind) is needed to destroy it.
Power: Enemy's attack power, how much damage does it deal to our player.
ShotAngle: to which direction the enemy shoots the bullet, it can be left or right or anywhere we want.
That's pretty much what we need for our enemy, now let's make the enemy class/object.
The Enemy class/object is pretty much the same as the player class/object, except the enemy doesn't listen to player input. Because of that we need to replace the parts where the hero listens to player input, to enemy AI/logic.
Enemy Attack AI
For starters, let's handle the enemy's basic shooting AI. The enemy will shoot at the player when it sees the player.
To determine whether the enemy "sees' the player, we will need to define a variable for the enemy object called facing which is a string that stores one of two values, "left" or "right".
The enemy also needs some kind of range of sight, which is why we're going to make another variable called range. If the player is within this range then that means the enemy "sees" the player. The pseudocode is as follows:
function boolean checkSees() { if (facing == "left" && hero.x >= enemy.x -- range) { return true; } if (facing == "right" && hero.x <= enemy.x + range) { return true; } return false; }
checkSees() function
Maybe you've noticed something in this pseudocode: it doesn't consider the hero's y position, so the enemy will still shoot at the hero even if they're at platforms with different heights.
For now this will suffice, because making a line of sight algorithm is outside the scope of this tutorial. In your own game, you might want to add a Y tolerance in that function above that will check whether hero's y position is between two points that define the enemy's heights.
Making Enemies Shoot
The pseudocode for enemy shooting is as following:
// can be in update() or somewhere else that's executed every frame function update() { if (checkSees()) { shoot("path/to/bulletSprite.png", enemyPower, 400, shotAngle); } }
As you can see, the enemy shoot() function is similar to that of the player's. It takes the sprite's path, attack power, bullet speed, and shooting angle as parameters.
Enemy Movement AI
When does the enemy switch from facing left to facing right? For our hero, we use player input to change the direction our hero faces. For our enemy we have two options: use some kind of timer to switch facing direction every few seconds while having the enemy to stand still, or have the enemy to walk to a certain spot and then switch its facing direction and then walk to another spot to switch its facing direction again.
This second method can be used as a patrolling AI. Of course, we can just make the enemy walk in one direction and never turn back.
The pseudocode for the first method is as follows:
function switchingAI() { // elapsedTime() is a function that counts how many seconds has passed since its value is reset // I assume your engine of choice have this kind of functionality if (elapsedTime() > 4.0) { if (facing == "left") { facing = "right"; shotAngle = 0; } if (facing == "right") { facing = "left"; shotAngle = 180; } enemy.mirrorSprite(); // also flip the sprite horizontally resetTime(); // resets the time that counts in elapsedTime() } }
Enemy Patrolling AI
To make the patrolling AI, we need to make two invisible objects that are at the end of both ways of enemy's patrolling route, and make the enemy move another way if it collides with them.
Now let's write our pseudocode for the enemy's patrolling AI:
function patrollingAI() { if (facing == "right") { walkRight(); // the same as the one in player object / class if (collidesWith(rightPatrolBorder)) { facing = "left"; enemy.mirrorSprite(); } } if (facing == "left") { walkLeft(); if (collidesWith(leftPatrolBorder)) { facing = "right"; enemy.mirrorSprite(); } } }
After this, the enemy will patrol between two points like we want it to.
To set up which AI the enemy uses, we're going to add one more variable with a string type for our enemy: enemy AI. This will determine what AI to use every frame, like so:
if (enemyAI == "switching") { switchingAI(); } else if (enemyAI == "patrolling") { patrollingAI(); }
Of course you can add more enemy AI type if you want.
Shot Variation
Let's go on about how we can make shot variations for both the player and the enemy. We're making shot variations by changing two things: the shooting angle, and the number of bullet shot.
This way we can make a simple one bullet shot, or a three directional bullet shot. Before we do this we're going to make another variable to the enemy object/class named shotAI, which is a string. We'll use this in our checkSees() checking if block, where the enemy shoots. The changes to that code block will be like this:
// can be in update() or somewhere else that's executed every frame function update() { if (checkSees()) { if (shotAI == "simple") { shoot("path/to/bulletSprite.png", enemyPower, 400, shotAngle); } if (shotAI == "threeBullets") { shootThreeBullets(); } } }
Of course, the name of the AI and what kind of shot the enemy would fire are up to you, this is just an example.
Now, let's delve deeper into what is inside the shootThreeBullets() function.
Function shootThreeBullets() { if (facing == "right") { shoot("path/to/bulletSprite.png", enemyPower, 400, 0); // this bullet goes straight to the right shoot("path/to/bulletSprite.png", enemyPower, 400, 330); // this goes up by 30 degrees shoot("path/to/bulletSprite.png", enemyPower, 400, 30); // this goes down by 30 degrees } if (facing == "left") { shoot("path/to/bulletSprite.png", enemyPower, 400, 180); // this bullet goes straight to the left shoot("path/to/bulletSprite.png", enemyPower, 400, 210); // this goes up by 30 degrees shoot("path/to/bulletSprite.png", enemyPower, 400, 150); // this goes down by 30 degrees } }
If you're unsure why 0 goes to right and 180 goes to left, it's because the direction of 0 degrees goes straight to the right side of the screen, 90 degrees goes to the down side of the screen, and so on until it hits 360 degrees. Once you know what value goes where, you can create your own shot variation.
We can also make a shotAI variable for the player, but I prefer we name it selectedShot, because our player will choose the bullet instead of programmed from the beginning. T
he logic in megaman is every time megaman defeats a boss, he gets that boss' power as a new shot. I'm going to try to recreate that logic. To do this we need an array that contains player's shots, including normal shots. The pseudocode is like this:
var shotArr = ["normalShot", "boss1", "boss2"]; var shotIndex = 0; var selectedShot = "normalShot"; function update() { // this is the block of code in player's update function where the player shoots a bullet // this function changes the bullet that the player shoots changeBullet(); // player press this key once if (keyPressed(KEY_Z)) { if (facing == "right") { hero.animate("Shoot"); if ( selectedShot == "normalShot") { hero.shoot("path/to/sprite.png", 10, 400, 0); } else if ( selectedShot == "boss1") { // add codes to shoot the kind of shot that the player received after defeating boss 1 } } } } function changeBullet() { // changes shotIndex based on button pressed if (keyPressed(KEY_E)) { shotIndex += 1; } if (keyPressed(KEY_Q)) { shotIndex -= 1; } // fix shotIndex if it's out of array's length if (shotIndex == shotArr.length) { shotIndex = 0; } if (shotIndex < 0) { shotIndex = shotArr.length -- 1; } selectedShot = shotArr[shotIndex]; }
We need to keep track of two new variables:
We will push new elements to shotArr when the player defeats a boss.
Upgrading the Player Bullets
Just like the enemy's shootThreeBullet(), you can be creative and create your own shot variations. Since this is the hero's bullet, let's give it something special.
Let's make one type of shot to be effective against a specific boss, so that it deals more damage. To do this, we'll create a variable for the bullet object named strongAgainst that is another string type variable that contains the name of the boss that this bullet is effective against. We will add this deals more damage functionality when we discuss the boss part of the game.
Health and Death
This is where all the shot variations we make really start to matter. This is where our hero damage and kill the enemy, and the other way around.
To begin, let's make a variable for both the hero and enemy object, named health which is an int, and another variable just for the hero named lives. Let's take a look at the pseudocode:
if (bullet.collidesWith(hero)) { hero.health -= bullet.power; createExplosion(); // for now we don't have an explosion sprite, so this will act as a reminder } // check if the hero is dead if (hero.health <= 0) { hero.lives -= 1; // decreases hero's total number of lives. destroyHero(); }
We will make the same pseudocode for damaging enemies, like so:
if (bullet.collidesWith(enemy)) { enemy.health -= bullet.power; createExplosion(); } if (enemy.health <= 0) { destroyEnemy(); }
The Health Bar GUI
Now, if I leave it at that then it would not be interesting. So I'll make a rectangle sprite on the top left corner of the screen that acts as our hero's health bar.
This health bar's length is going to change depending on our hero's current health. The formula for changing health bar's length is this:
// this is in the update function healthBar.width = (hero.health / hero.maxHealth) * 100;
We need one more variable for our hero called maxHealth; our hero's full health value. For now this value cannot be changed but maybe in the future we can create an item that increases the amount of hero's maxHealth.
Create The Game World
Now that we have created our hero, enemy, and shot variations, we need to make multiple levels and bosses.
To have multiple levels means that at some point in the game the player is going to reach one or more checkpoints that switches the game from level 1-1 to level 1-2 to level 1-3 and so on until they reach the boss.
When the player dies somewhere in level 1-2 he or she doesn't need to replay all the way back from the beginning of level 1-1. How do me do this? First we're going to make the level, I'm not going to explain much about level designing, but here is the example level in Construct 2.
The image in the top left corner is the HUD layer. It will scroll, following the hero when the game is played.
Doors and Checkpoints
One sprite you should pay attention to is the green sprite in the upper right part of the level. It is the checkpoint in this level when the hero collides with it we will transfer the game on to level 1-2.
To handle multiple levels we need three variables: currentLevel, levelName, and nextLevel.
The currentLevel variable is created in the hero object/class. The levelName is created in the game scene (level) object for each level. The nextLevel variable is created in the green sprite object.
The logic is as follows: when the hero collides with the green sprite (I call it greenDoor), we will change our level to the game scene in which levelName is the same as the nextLevel variable. After we change the level we will change the value of hero's currentLevel variable to the same as game scene's levelName. Here's the pseudocode:
// this is inside game's update function if (hero.collidesWith(greenDoor)) { changeLevelTo(greenDoor.nextLevel); }
Initializing A New Level
Here is the pseudocode for dealing with when the next level is loaded and ready to play.
// this is the function that's triggered when the new level is loaded function onStart() { hero.currentLevel = scene.LevelName; hero.x = startPos.x; hero.y = startPos.y; }
The Player Start Positions
Now that we have changed to a new level I will explain the orange sprite behind our hero in the level design image above. That orange sprite is an object that I call startPos. It is used to mark the starting position of each level.
We refer to this object when the hero just changed levels or died, so that we know where to spawn him.
Here is the pseudocode for handling when the hero dies:
// the function that's triggered when the hero is destroyed Function onDestroyed() { // revives hero if the hero still have lives. If (hero.lives > 0) { var newHero = new Hero(); newHero.x = startPos.x; newHero.y = startPos.y; } }
Now we can have multiple levels and we can also respawn the hero after he dies.
You can create as many levels as you want, or maybe even create two greenDoor objects in a level which one of them goes back to level 1-1 if the player solve a puzzle in a wrong way.
Boss Battles
It is finally time to implement the boss itself. To make a boss level is as simple as making another level which will spawn a boss instead of regular enemies.
The hard part is creating AI for the boss because each boss will have a unique AI. So to make it easy to understand and duplicate for a lot of bosses, I'm going to make the boss AI be dependent on the time after they're spawned. Which means they're going to do A for x seconds, then change to B for y seconds then doing C for z seconds before returning back to A. The pseudocode will look something like this:
// this code is inside the boss update function, so it's executed every frame if (elapsedTime() > 2.0) { // this if block is executed for 3 seconds, because the difference in time with the if block below // is three seconds. BossShot1(); // shot variation to be executed this time } else if (elapsedTime() > 5.0) { bossShot2(); } else if (elapsedTime() > 6.0) { bossShot3(); } if (elapsedTime() > 7.0) { // reset the time so the boss executes the first action again resetsTime(); }
The definition of the boss shot functons is below. Feel free to change it to fit what you want for a boss fight:
function bossShot1() { // a simple straight shot bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle); // shotAngle is 180 } function bossShot2() { // a three direction bullets shot bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle); bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle + 30); bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle - 30); } function bossShot3() { // for this one, I'm going to make a circle shot, so the bullets will form a circle for (var i = 0; i <= 9; i++) { bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, 36 * i); // change the shotAngle } }
It is up to you to add shot variations to the boss' routine. The variables used by the boss enemy object are the same as enemy object, except that bosses don't use the enemyAI and shotAI variables, since both will be handled in the if block above depending on the elapsed time.
Boss enemies also have a variable that enemy objects don't. It is called rewardShot, which is a string. This variable holds the name of a boss shot that the player will obtain after defeating the boss (the ones in shotArr array variable).
This will allow the player to "learn" the boss attack as explained earlier. To add this shot type to the array of player shots we will need to add the following code after the enemy dies:
function bossShot1() { // a simple straight shot bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle); // shotAngle is 180 } function bossShot2() { // a three direction bullets shot bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle); bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle + 30); bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, shotAngle - 30); } function bossShot3() { // for this one, I'm going to make a circle shot, so the bullets will form a circle for (var i = 0; i <= 9; i++) { bossEnemy.shoot("path/to/bullet/sprite.png", bossPower, 400, 36 * i); // change the shotAngle } }
To implement additional bullet types for your players to enjoy, all you need to do is add the appropriate code for each rewardShot your create, just like we did earlier.
Congratulations!
You have completed my tutorial on how to create a Megaman-like metroidvania platformer game in Construct 2. The rest is just building up your game based on what you have learned here to make it your own. Add more enemy types, more levels, powerups and more. Good luck and have fun.