How to Make a Push Puzzle – Let’s Create a Sokoban-Style Puzzle (Second Half)

Game

Let’s make a simple game using just HTML and JavaScript, without any game engines or libraries.

This is for people who:

Want to try making a game but have no idea where to start.
Just want to make a simple game without diving into complex tools like Unity or Unreal Engine.

This guide is aimed at those who already have some programming knowledge.

Articles to Read First

The creation of the basic logic for Sokoban is explained in the first part. It’s recommended to read that first.

What We’ll Make in This Article

This is a version where the squares and circles from the first part have been changed to pixel art. This change alone has made the game look a lot better.

There is an additional element called “door” that is not in the original. The door opens when all the boxes are placed in the designated spots.

This version is quite similar to the original, so I won’t be releasing a playable version. Please read the article and try to create the finished version yourself.

Source Code

Only the main differences from the first part are included.

const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false;

const SCREEN_W = 20
const SCREEN_H = 15
const BLOCK_SIZE = 32

const Const = {
    NONE: 0,
    WALL: 1,
    BOX: 2,
    POINT: 3,
    PLAYER: 4,
    DOOR: 5,
    GOAL: 6,
}

const PlayerDir = {
    LEFT: 0,
    RIGHT: 1,
    UP: 2,
    DOWN: 3,
}

class Obj {
    constructor(x,y) {
        this.x = x;
        this.y = y;
    }
}

const stages = [
// omitted
];

let stage_i = 0;
let map;

let player = new Obj(0,0);
let player_dir = PlayerDir.LEFT;

let box_list = [];


// Input event handler
document.addEventListener('keydown', keyDownHandler, false);

function keyDownHandler(event) {
    if (event.key === 'i') { // Up
        movePlayer(0, -1);
        player_dir = PlayerDir.UP;
    } 

    if (event.key === 'm') { // Down
        movePlayer(0, 1);
        player_dir = PlayerDir.DOWN;
    }

    if (event.key === 'j') { // Left
        movePlayer(-1, 0);
        player_dir = PlayerDir.LEFT;
    }

    if (event.key == 'l') { // Right
        movePlayer(1, 0);
        player_dir = PlayerDir.RIGHT;
    }

    if (event.key == 'r') { // Right
        init();
    }
}

// ・・・・omitted

function drawSprite(sprite_no, x, y) {
    context.drawImage(spriteSheet, sprite_no*8, 0, 8, 8, x*BLOCK_SIZE, y*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}

function drawWall(x, y) {
    drawSprite(1, x, y);
}

function drawDoor(x, y) {
    drawSprite(3, x, y);
}

function drawPoint(x, y) {
    drawSprite(2, x, y);
}

function drawPlayer() {
    drawSprite(4 + player_dir, player.x, player.y);
}

function drawBox() {
    for (const item of box_list) {
        drawSprite(0, item.x, item.y);
    }
}

function drawScreen() {
    for (j = 0; j < SCREEN_H; j++) {
        for (i = 0; i < SCREEN_W; i++) {
            obj = map[ i + j*SCREEN_W ];
            switch(obj) {
            case Const.WALL:
                drawWall(i, j); break;
            case Const.POINT:
                drawPoint(i, j); break;
            case Const.DOOR:
                drawDoor(i, j); break;
            default:
                break;
            }
        }
    }
}

function isGameClear() {
    for (const item of box_list) {
        if (map[item.x + item.y * SCREEN_W] != Const.POINT) {
            return false;
        }
    }
    return true;
}

function init() {
    map = Array.from(stages[stage_i]);
    player_dir = PlayerDir.LEFT;
    box_list = [];
    for (y = 0; y < SCREEN_H; y++) {
        for (x = 0; x < SCREEN_W; x++) {
            obj = map[x + y*SCREEN_W];
            switch (obj) {
            case Const.PLAYER:
                player.x = x;
                player.y = y;
                break;
            case Const.BOX:
                box_list.push(new Obj(x,y));
                break;
            }
        }
    }
}

function openTheDoor() {
    for (i = 0; i < SCREEN_W*SCREEN_H; i++) {
        if (map[i] == Const.DOOR)
            map[i] = Const.NONE;
    }
}

function update() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    drawScreen();
    drawBox();
    drawPlayer();
    if (isGameClear()) {
        openTheDoor();
    }
    if (hitObj(player.x, player.y, Const.GOAL)) {
        stage_i = (stage_i+1) % 2;
        init();
    }
}

// load the sprite sheet
const spriteSheet = new Image();
spriteSheet.src = "./spritesheet.png";

init();

setInterval(update, 10);

How to Display Pixel Art

First, create a PNG file of the pixel art. There are many graphics editors that allow pixel art editing, so feel free to use whichever you like. I use Firealpaca, and I can recommend it.

The image data prepared is shown below. The actual image size is quite small, so it’s enlarged for the blog. The gray part of the background is actually specified as a transparent color.

An 8×8 pixel image is called a sprite. In the program, a function is created to specify the sprite number and draw it.

Save this image as “spritesheet.png” in the same folder as “index.js”.

Then, load the PNG file from JavaScript.

// load the sprite sheet
const spriteSheet = new Image();
spriteSheet.src = "./spritesheet.png";

By doing this, the image data is stored in an instance of the Image object named spriteSheet.

To draw the image data, use the context.drawImage() function. The specifications are as follows:

context.drawImage(
    image,      // Image object
    sx,         // X coordinate to crop within the image
    sy,         // Y coordinate to crop within the image
    sWidth,     // Width to crop
    sHeight,    // Height to crop
    tx,         // X coordinate on the Canvas
    ty,         // Y coordinate on the Canvas
    tWidth,     // Width to draw
    tHeight     // Height to draw
);

In the actual game, the process is handled by the following function:

function drawSprite(sprite_no, x, y) {
    context.drawImage(spriteSheet, sprite_no*8, 0, 8, 8, x*BLOCK_SIZE, y*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}

What it does is cut out a specific area from the image data of spritesheet.png and draw it enlarged on the canvas. Illustrated below:

The Fun and Tedious Parts of Making Games

When making a game, there are fun parts and tedious parts. What is enjoyable and what is boring varies from person to person.

For me, game programming is extremely fun, but everything else feels very tedious. The part about creating graphics is one of them. To be honest, drawing an 8×8 pixel character is the limit of my patience. I don’t feel like making anything more than that.

However, even if it’s simple pixel art, having it makes a world of difference. The game looks much better now compared to the previous version with just squares and circles.

Even the 8×8 pixel world is not bad, I thought again this time.

How to Enjoy It After Making It

So far, we’ve seen how to make a Sokoban game. It’s a very simple game, so I don’t think it’s difficult to make.

However, the fun part of this game is the “stage data.” The game system is a puzzle rule, but the puzzle itself is the “stage data.”

If you make it and like it, I think it’s the royal road to buy and play the original.

What’s amazing about “Sokoban” is that more than 40 years have passed since its release in the 1980s, and it is still being sold. This game is extremely simple, but it can be said to be a masterpiece that remains in game history.


コメント

Copied title and URL