On YouTube, someone asked me how to implement player sprite rotation in a top-down shooter game.
This tutorial will teach you how to implement that and in addition, we will tackle how to shoot bullets towards enemies, add effects to provide visual feedback, etc…
This tutorial assumes basic knowledge of Kaplay, a JS library for making games quickly. Check out my YouTube channel for beginner friendly tutorials.
How to Rotate the Player Sprite According to the Mouse Position
Let’s first start with some basic code to set up our Kaplay canvas and load our sprites. Sprites are pulled from here.
const k = kaplay({
global: false,
background: [100, 100, 200],
touchToMouse: true,
});
k.loadSprite("player", "./player.png");
k.loadSprite("enemy", "./enemy.png");
Here, I simply set the background to a purple color and I make the game playable on mobile by setting “touchToMouse” to true.
Let’s then create our game scene and set it up as the default scene. We will also create a game over scene to transition the player to it when they die, giving them the ability to restart the game.
k.scene("game", () => {
});
k.scene("gameover", () => {
});
k.go("game");
For our game scene, we will zoom the camera a bit and create our player game object.
k.scene("game", () => {
k.camScale(1.5);
const player = k.add([
k.sprite("player"),
k.anchor("center"),
k.area(),
k.pos(k.center()),
k.rotate(0),
k.health(6),
"player",
]);
});
The use of the “rotate” component allows the use of the “rotateTo” method to rotate the player sprite according to the mouse position. Add the following code to the game scene :
player.onUpdate(() => {
player.rotateTo(k.mousePos().angle(player.pos));
});
By passing to the “rotateTo” method the angle between the position of the mouse and the player’s position, we’re able to make the player game object rotate towards where the mouse is.
The “mousePos” Kaplay function returns a Vec2, a data structure provided by Kaplay which has the “angle” method available. This method computes the angle between two Vec2s.
You should have the following result :
How to Implement Bullet Shooting
To spawn bullets and shoot them towards the current mouse position, you need to add the following code to the game scene :
k.onClick(() => {
k.add([
k.rect(10, 10, { radius: 6 }),
k.pos(player.pos),
k.area(),
k.anchor(k.vec2(-2, -2)),
k.offscreen({
destroy: true,
}), // will destroy the bullet when it exits visible area of the screen
k.rotate(player.angle),
k.move(k.mousePos().sub(player.pos).unit(), 1200),
"bullet",
]);
});
We have an onClick that creates a bullet game object every time the player clicks. We set the position of the bullet to be the same as the player. We also rotate the bullet using the same angle as the player. We use the “move” component to make the bullet move towards the mouse cursor.
This is achieved by computing a direction Vec2 which determines in which direction the bullet will go. It’s computed by subtracting the mouse position by the player’s position and then making sure the resulting Vec2 is of length 1 by using the “unit” method. This is called normalizing. We want to do this because the “move” component will scale the vector using its second param serving as our bullet’s speed.
The most crucial part of this code snippet is the use of the “anchor” component to place bullets near the tip of the gun. The anchor component determines the origin of a game object. By default, game objects are drawn from the top-left which is the default set origin. By using anchor(“center”), the game object will be drawn from its center. It’s not well known, but you can pass to it a Vec2 instead of a string. By passing a Vec2, the game object will use that as its origin.
By using the anchor component this way, you can make sure the bullet will always spawn at the tip of the gun regardless of the current orientation of the player.
You can see the anchor in debug mode which you can activate using the “f1” key. Note that you can set any key you want for enabling debug mode instead of “f1” by doing “kaplay({ debugKey: “d”})” with the key you want to use.
The result, is the following :
How to Implement Enemy Spawning
For spawning enemies and making sure they go towards the player, we first need a reusable function. You can add this function in the game scene or put it in another file and export it when needed.
function makeEnemy() {
const spawnPoints = [
k.vec2(k.width() / 2, k.height()),
k.vec2(0, k.height()),
k.vec2(k.width(), k.height()),
k.vec2(0, k.height() / 2),
k.vec2(k.width(), 0),
k.vec2(0, -k.height()),
k.vec2(0, -k.height() / 2),
];
const selectedSpawnPoint = spawnPoints[k.randi(spawnPoints.length)];
const enemy = k.add([
k.sprite("enemy"),
k.anchor("center"),
k.area(),
k.pos(selectedSpawnPoint),
k.rotate(0),
k.health(3),
]);
enemy.rotateTo(player.pos.angle(enemy.pos));
enemy.onUpdate(() => {
enemy.moveTo(player.pos, 100);
});
}
// this will spawn a new enemy every second
k.loop(1, () => {
makeEnemy();
});
We first create an array of possible positions where we can spawn an enemy. We then randomly select one of these positions by using the “randi(spawnPoints.length)”. In case you didn’t know, “randi” is a Kaplay function that picks a random integer between 0 and the value specified. The upper bound of this range is not included, so it will never be picked. That’s why I use “spawnPoints.length” and not “spawnPoints.length - 1” in the code.
Once the code is added you should have the following result :
How to Implement Collisions and Damage Calculations
Now has come the time to implement collisions. We need to make sure that the enemy gets destroyed if hit by bullets 3 times. Why 3 times, that’s just a design decision I made.
We also need to destroy the enemy when colliding with the player while also inflicting damage to the player.
You probably noticed that we used the “health” component in both enemy and player game object definitions. This will allow us to easily manage their health. By using this component we get access to the “hp”, “hurt” and “heal” methods.
You also probably noticed that we had a “player” tag in the player’s game object definition. This is so we can listen for collisions between the enemy and the player. The same logic will apply for bullets, since we added the “bullet” tag to their game object definitions as well.
Add the following code within the “makeEnemy” function :
function makeEnemy() {
//...previous code
enemy.onCollide("player", (player) => {
player.hurt(1);
k.destroy(enemy);
k.shake(10);
if (player.hp() === 0) {
k.go("gameover");
}
});
enemy.onCollide("bullet", (bullet) => {
k.destroy(bullet);
if (enemy.hp() > 0) {
enemy.hurt();
enemy.use(k.color(200, 0, 0));
k.wait(0.2, () => enemy.unuse("color"));
return;
}
k.destroy(enemy);
});
}
The first onCollide will check if the enemy collides with the player. If yes, we reduce the health of the player by 1 and destroy the enemy. We also make the screen shake to provide visual feedback of the player taking damage. Finally, we check if the player’s health is now 0. If yes, we go to the game over scene.
The second onCollide will check if the enemy collides with a bullet. If yes, the bullet is destroyed. If the enemy’s health is above 0 we reduce it by 1. We change the color of the enemy sprite by adding a “color” component. We remove it shortly after 0.2 seconds. This will provide visual feedback of the enemy taking damage. If instead, the enemy’s health was below or equal to 0, the enemy would be destroyed.
You should have the following result :
Finishing Touches
To enable the player to play again after a game over, we can add the code below in the gameover scene logic. This will restart the game when the player clicks anywhere.
k.scene("gameover", () => {
k.onClick(() => k.go("game"));
});
That’s it! Hope you learned something new. Before you leave, I just want to inform you that I made a samurai pixelart themed asset pack that you can buy for $4 on itch.io. Click here for more info if you’re interested.
Subscribe for more content like this!
In the meantime, you can read my previous posts.
Great article! Give valuable insights on my work, too. The integration of EchoAPI with JavaScript projects enhances my workflow by providing instant feedback on API calls.