This guide is for those who want to create a game using only JavaScript, without using game engines or libraries.
This is for people who:
Want to make a game but don’t know where to start.
Want to create a simple game without the hassle of using Unity or Unreal Engine.
This guide is aimed at those who already have some programming knowledge.
The Completed Game
This is the final version of the Lode Runner-style action puzzle game.
Avoid the blue monsters and collect all the coins on the stage. A hidden ladder will appear, and if you climb to the top, you clear the stage. Note, there’s only one stage.
Controls:
s … Start the game
x … Dig left, c … Dig right
j … Move up, m … Move down, j … Move left, l … Move right
r … Restart the stage
Articles to Read Before This One
Source Code
Changes Since Last Time
Here’s what’s been added since the previous demo:
- Adding coins and related elements
- The process for the player to collect coins
- The process for the monster to collect coins, and to drop coins when it falls into a hole
- Logic for displaying a hidden ladder when the player collects all coins
- The process for the monster to die if buried in a block
- The process for the player to die when touched by a monster
- The process for clearing the stage when the player climbs to the top of the hidden ladder
- Title screen
- Game clear screen
- Managing game states and screen transitions
Adding Coins and Related Game Elements
We’ve added coins as a new map element.
const Elem = {
...
COIN: 5,
...
}
They’re defined as number 5. Let’s look at the map
data defined in index.js
:
const map = [
//1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
1,6,0,0,0,0,0,0,0,0,0,0,0,0,0,1, // 1
1,6,0,0,0,0,0,0,0,0,0,0,0,0,0,1, // 2
1,6,0,0,0,0,0,0,0,3,3,3,3,3,3,1, // 3
1,1,1,1,1,1,1,1,2,0,0,0,0,0,5,1, // 4
1,0,3,3,3,3,0,0,2,0,5,0,0,7,0,1, // 5
1,2,0,2,0,2,1,0,2,1,1,1,1,1,1,1, // 6
1,5,1,5,1,5,1,0,2,1,0,0,0,0,0,1, // 7
1,1,1,1,1,1,1,0,2,1,0,1,1,1,0,1, // 8
1,1,5,5,5,1,0,2,1,1,0,1,0,0,0,1, // 9
1,1,1,1,1,1,0,2,1,0,5,1,2,1,1,1, // 10
1,0,0,3,3,3,0,2,1,0,1,1,2,1,5,1, // 11
1,2,1,5,5,5,1,2,0,5,1,1,2,1,0,1, // 12
1,2,4,4,4,4,4,2,0,1,1,1,2,1,1,1, // 13
1,2,0,0,8,0,0,2,0,0,0,0,2,1,1,1, // 14
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 15
4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, // 16
];
This map
array is converted to a JavaScript object when the game starts. It’s an instance of the class ECoin
.
const State = {
NORMAL: 'NORMAL',
}
const anime_table = {
NORMAL: { frames: [24,25,26,27], frame_interval: 10},
}
export class ECoin {
constructor() {
this.state = State.NORMAL;
this.anime_index = 0;
this.frame_count = 0;
this.sprite = 24;
}
can_go_through() { return true; }
can_up() { return false; }
can_stand_on() { return false; }
can_hang() { return false; }
can_pick_up() { return true; }
is_dig_hole() { return false; }
sprite_no() { return this.sprite; }
update () {
!this.anime_update();
}
anime_update() {
let frames = anime_table[this.state].frames;
if (this.anime_index >= frames.length) {
this.anime_index = 0;
return false;
}
if (this.frame_count >= anime_table[this.state].frame_interval) {
this.anime_index++;
this.frame_count = 0;
}
this.frame_count++;
if (this.anime_index < frames.length) {
this.sprite = frames[this.anime_index];
}
return true;
}
}
The coins spin, so there’s an animation process. (See anime_table
and anime_update()
.)
During the generation of the map elements in the class World
constructor, the number of coins is counted. By comparing this number with the number of coins the player collects, we can check if the game clear condition is met.
function countCoins(m) {
let n = 0;
for (let id of m) {
if (id == Elem.COIN) n++;
}
return n;
}
・・・・
export class World {
constructor(w,h, data, bg=true) {
・・・
this.num_of_coins = countCoins(data);
・・・
}
The Process for the Player to Collect Coins
When the player moves, it checks if it has reached the same coordinates as a coin, then proceeds to collect the coin.
・・・
export class Chara {
constructor(x,y, world) {
・・・
this.hold_coins = 0;
}
update() {
・・・
if (!this[action_func]()) {
this.check_get_coin();
・・・
}
・・・
check_get_coin() {
if (this.world.isOnCoin(this.x, this.y)) {
this.world.pickUp(this.x,this.y);
this.hold_coins++;
if (this.hold_coins == this.world.numOfCoins()) {
this.world.showHideLadder();
}
}
}
・・・
Let’s look at the implementation of the check_get_coin()
function.
The process for collecting coins involves removing the coin object from the map (this.world.pickUp()
) and adding to the player’s hold_coins
value (this.hold_coins++
).
Each time a coin is collected, it checks if the number of coins the player holds matches the total number of coins (this.world.numOfCoins()
). If they match, the hidden ladder is shown (this.world.showHideLadder()
).
The Process for the Monster to Collect and Drop Coins
Monsters also collect coins, but only one. (I think this element adds a bit of spice to the game.) It’s almost the same as class Chara
.
update() {
・・・
let action_func = `action_${this.state.toLowerCase()}`;
if (!this[action_func]()) {
this.check_get_coin();
if (this.can_fall()) {
・・・
}
・・・
check_get_coin() {
if (this.hold_coins > 0)
return;
super.check_get_coin();
}
In check_get_coin()
, if the monster already has more than 0 coins, it won’t pick up another. After that, it calls super.get_check_coin()
, which is the get_check_coin()
of class Chara
. Remember that class Enemy
inherits from class Chara
. To call the same function from the parent class, use super.functionName()
.
When the blue monster falls into a hole dug by the player, it drops the coin above itself. This is implemented in the following part. It’s so simple that no explanation is needed.
can_fall() {
if (this.world.isDigHole(this.x, this.y)) {
this.change_state(State.IN_HOLE);
if (this.hold_coins > 0) {
this.world.putCoin(this.x, this.y-1);
this.hold_coins--;
}
return false;
}
return super.can_fall()
}
The Process for the Monster to Die
The monster dies if it falls into a hole and gets buried by a block that repairs itself.
The implementation is simple, as shown in if (this.check_dead()) { ... }
.
check_dead() {
return !this.world.canGoThrough(this.x,this.y);
}
・・・
action_in_hole() {
let ret = this.count_move(0, 0);
if (!ret) {
this.change_state(State.UP_HOLE);
}
if (this.check_dead()) {
this.change_state(State.DEADING);
}
return true;
}
・・・
With this implementation, if the monster is in the process of climbing out of a hole and the block fully repairs itself, it’s safe. (This is a matter of game balance; if you want to change it, just call check_dead()
inside action_up_hole()
.)
The Process for the Player Character to Die
The player character dies under the following conditions:
- When touched by a monster
- When buried after falling into a hole
When either of these states is detected, the player performs a DEATH animation and then returns to the title screen. This is implemented in the part below.
・・・
update() {
if (this.check_dead()) {
this.change_state(State.DEADING);
}
・・・
}
check_dead() {
if (this.state == State.DEADING || this.state == State.DEAD)
return false;
if (this.world.isOnEnemy(this.x, this.y, this.w, this.h)) {
return true;
}
if (!this.world.canGoThrough(this.x, this.y)) {
return true;
}
return false;
}
・・・
Apart from this, there may be situations where the player is trapped and can’t move, making it impossible to clear the game.
This can happen if you destroy the blocks in the wrong order in the puzzle part. In this case, there’s a stage reset option (in this game, it’s the “r” key).
Title Screen and Clear Screen
Apart from the in-game state (State.GAME
), we’ve added two special states:
- Title screen (
State.TITLE
) - Game clear screen (
State.GAME_CLEAR
)
Even the simplest retro games have these two elements. The title screen is the gateway to the game, so you want to create an exciting atmosphere. The game clear screen should give a sense of achievement.
This game achieves a certain atmosphere with minimal implementation.
Title Screen
The player character is stationary and animated, while the monster walks from the right edge toward the player and continues animating even after stopping.
No special implementation is done for this scene. It uses the game engine directly, prepares a map for this screen, and sets up the blocks to create this situation.
Then, it overlays the title image data and the text “Press ‘S’ Key to Start” on top.
Game Clear Screen
This screen only shows characters and coins lined up and animating, but I think it’s much better than just displaying the “Game Clear” text.
Here, I did a little trick. Like the title screen, this screen uses a dedicated map for placement. However, if the game engine runs as is, the player character would fall.
To prevent this, I placed invisible blocks underneath the character. The trick is that I prepared transparent blocks that aren’t part of the game elements. Transparent blocks could be utilized in the game elements depending on the idea, so they aren’t purely useless.
As you can see, rather than writing dedicated code for these screens, I find joy in using small ideas to achieve the same result with minimal logic.
Conclusion
Over these five parts, we’ve built a Lode Runner-style action puzzle game. As you play, you may notice some rough edges that are close to bugs.
- You can erase blocks below ladders
- When hanging on a bar and erasing a block, the player ends up standing
- The player can move, leaving only the laser
You might find more. These can be fixed if you want, but I haven’t done it because it’s just a hassle.
If you like it, the source code is open, so feel free to tweak it as a way to pass the time.
コメント