How to Make an Action Puzzle Game – Inspired by Lode Runner (Part 4)

Game

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.

What We’ll Make in This Article

We’re introducing the enemy character, a light blue monster. The light blue monster will chase you endlessly. Even if it catches you, it won’t be game over, so try running away in different ways. You can destroy blocks with a laser, and they will automatically repair themselves after a while.

操作方法:
s … Start the Game
x … Dig Left, c … Dig Right
j … Up, m … Down, j … Left, l … Right
r … Reset the Game

Articles to Read Before This One

Source Code

how-to-make-lode-runnder-like-game/03_chara_w_monster at main ?? yenshan/how-to-make-lode-runnder-like-game
Contribute to yenshan/how-to-make-lode-runnder-like-game development by creating an account on GitHub.

Here’s the structure of the demo’s files:

.
├── Block.js
├── Chara.js
├── Enemy.js
├── World.js
├── index.html
├── index.js
└── spritesheet.png

Since last time, we’ve added the Enemy.js file. This time, we’ll mainly explain Enemy.js.

import {drawSprite} from "./index.js"
import {Chara} from "./Chara.js" 

const STAY_HOLE_SEC = 3;

const State = {
    STOP: 'STOP',
    MOVE_LEFT : 'MOVE_LEFT',
    MOVE_RIGHT: 'MOVE_RIGHT',
    MOVE_UP : 'MOVE_UP',
    MOVE_DOWN : 'MOVE_DOWN',
    FALL:  'FALL',
    STOP_LADDER: 'STOP_LADDER',
    MOVE_BAR_LEFT : 'MOVE_BAR_LEFT',
    MOVE_BAR_RIGHT : 'MOVE_BAR_RIGHT',
    STOP_BAR : 'STOP_BAR',
    IN_HOLE: 'IN_HOLE',
    UP_HOLE: 'UP_HOLE',
}

const anime_table =  {
    STOP: {move_count: 0, frames: [50,51], frame_interval: 60},
    MOVE_LEFT: {move_count: 8, frames: [50,52,53], frame_interval: 3},
    MOVE_RIGHT: { move_count: 8, frames: [50,52,53], frame_interval: 3},
    MOVE_UP: {move_count: 8, frames: [55,56], frame_interval: -1},
    MOVE_DOWN: {move_count: 8, frames: [55,56], frame_interval: -1},
    FALL: {move_count: 8, frames: [49,56], frame_interval: 2},
    STOP_LADDER: {move_count: 8, frames: [55,56], frame_interval: -1},
    MOVE_BAR_LEFT: {move_count: 8, frames: [48,49], frame_interval: -1},
    MOVE_BAR_RIGHT: {move_count: 8, frames: [48,49], frame_interval: -1},
    STOP_BAR: {move_count: 8, frames: [48], frame_interval: -1},
    IN_HOLE: {move_count: 30*STAY_HOLE_SEC, frames: [50], frame_interval: 1},
    UP_HOLE: {move_count: 8, frames: [50,52], frame_interval: 1},
};

export class Enemy extends Chara {

    constructor(x,y, map, chara) {
        super(x,y,map);
        this.sprite = 50;
        this.anime_table = anime_table;
        this.chara = chara;
        this.action_interval = 1;
        this.action_wait_count = 0;
    }

    wait_for_action() {
        if (this.action_wait_count < this.action_interval) {
            this.action_wait_count++;
            return true;
        }
        this.action_wait_count = 0;
        return false;
    }
    
    update() {
        if (this.wait_for_action())
            return;

        let action_func = `action_${this.state.toLowerCase()}`;

        if (!this[action_func]()) {
            if (this.can_fall()) {
                this.change_state(State.FALL);
            } else {
                this.think_next_action();
            }
        }

        this.anime_update();
    }

    think_next_action() {
        let chara = this.chara;

        if (chara.y == this.y) {
            if (chara.x < this.x) {
                this.check_move_left();
            } else {
                this.check_move_right();
            }
            return;
        }

        if (chara.y > this.y) { 
            if (this.check_move_down()) return;
        } else {
            if (this.check_move_up()) return;
        }

        switch(this.state) {
        case State.MOVE_LEFT:
            if (!this.check_move_left()) this.check_move_right();
            break;
        case State.MOVE_RIGHT:
            if (!this.check_move_right()) this.check_move_left();
            break;
        default:
            if (chara.x < this.x) {
                this.check_move_left();
            } else {
                this.check_move_right();
            }
        }

    }

    check_move_down() {
        if (this.map.isDigHole(this.x, this.y))
            return false;
        return super.check_move_down();
    }

    can_fall() {
        if (this.map.isDigHole(this.x, this.y)) {
            this.change_state(State.IN_HOLE);
            return false;
        }
        return super.can_fall()
    }

    action_in_hole() {
        let ret = this.count_move(0, 0);
        if (!ret) {
            this.change_state(State.UP_HOLE);
        }
        return true;
    }

    action_up_hole() {
        let ret = this.count_move(0, -1);
        if (!ret) {
            if (this.chara.x < this.x) {
                this.change_state(State.MOVE_LEFT);
            } else {
                this.change_state(State.MOVE_RIGHT);
            }
        }
        return true;
    }
}

Inheritance Relationship Between Chara and Enemy Classes

The light blue monster is implemented with the Enemy class. The actions Enemy can take on the game field are almost the same as the player. The only difference is that it doesn’t pass through the holes dug by the player (it falls into them).

The implementation is almost the same as class Chara. You could copy the entire class Chara to create class Enemy and make the changes, but it’s better to manage the same implementation code in one place. So, we use the inheritance feature of object-oriented JavaScript.

You implement inheritance using extends:

class Enemy extends Chara {
・・・

The inheritance relationship looks like this:

Since Enemy inherits from Chara, it can use all of Chara‘s features as they are. Then, we just need to add the parts that differ from Chara.

Ideally, we should extract only the common parts of the player Chara class and the Enemy class into a separate parent class for proper design. This is because the Chara class defines methods for player key input that aren’t needed in the Enemy class.

However, this game won’t introduce more characters, and there’s no benefit in strictly classifying and managing them, so we’re leaving it as is.

Logic for Auto-Following the Player

In the field created for this demo, the light blue monster can chase the player endlessly. I think it moves quite decently.

What it’s actually doing is setting its movement state to approach the player while moving.

Within the function below, the monster decides which direction to move next.

    think_next_action() {
   ・・・
    }

The monster’s thought routine is simple, almost too simple:

  • If it’s at the same height as the player, it moves toward the player.
  • If it’s at a different height, it checks if it can move up or down toward the player and moves if possible.
  • Otherwise, it continues in its current direction until it hits a dead end, then moves in the opposite direction.

With this algorithm, if it faces a wall while at the same height as the player, the light blue monster will end up staying on the other side of the wall.

You could make it smarter, but that might make the game harder. You can try it on various maps and adjust it if it feels off.

Falling into and Escaping from Holes

One behavior where the enemy character differs from the player is that it doesn’t pass through the holes dug by the player (it falls into them). After falling into a hole, it waits a while and then escapes on its own.

You need to create these two states:

  • Falling into the hole (State.IN_HOLE)
  • Climbing out of the hole (State.UP_HOLE)

State transitions are as shown in the diagram below.

In the implementation, we add states to realize this and define corresponding animations.

const State = {
    ・・・
    IN_HOLE: 'IN_HOLE',
    UP_HOLE: 'UP_HOLE',
}

const anime_table =  {
  ・・・
    IN_HOLE: {move_count: 30*STAY_HOLE_SEC, frames: [50], frame_interval: 1},
    UP_HOLE: {move_count: 8, frames: [50,52], frame_interval: 1},
};

Here are the movement functions for each state:

    action_in_hole() {
        let ret = this.count_move(0, 0);
        if (!ret) {
            this.change_state(State.UP_HOLE);
        }
        return true; 
    }

    action_up_hole() {
        let ret = this.count_move(0, -1);
        if (!ret) {
            if (this.chara.x < this.x) {
                this.change_state(State.MOVE_LEFT);
            } else {
                this.change_state(State.MOVE_RIGHT);
            }
        }
        return true; 
    }

What’s Next

Finally, we’ve introduced enemy characters. The only remaining element is the “gold bars.”

Next time, we’ll implement the part where the player collects gold bars on the field to clear the game. We’ll also add an element where touching an enemy character results in a game over.

Next time, we’ll complete the game.


コメント

Copied title and URL