6. January 2019

How to write game with Vue.js – Sokoban – Part 4 – Move avatar by mouse click

In previous article we displayed also game objects. Now it’s time to add some movement.

We will use mouse click to navigate an avatar around the stage. It seems pretty straightforward, but there are several gotchas. Let’s explore them.

First of all we need to revisit the topic of Vuex store which we mentioned in the first article. It’s a very simple yet powerful design. All information about the state of the game is stored in the store. When you’d like to change something you dispatch a commit from your code and store methods will create a new state. In Vuex store these methods are known as mutations.

Let’s design simple mutation moveObject where we will send game object and two vectors in file js/game.js.

const store = new Vuex.Store({
...
    mutations: {
        moveObject(state, { gameObject, vectorX, vectorY }) {
            gameObject.x += vectorX;
            gameObject.y += vectorY;
        },
    }
});

You may wonder about interesting syntax of moveObject. The second attribute is an object which contains three attributes. If you write code this way it will allow you to pass more than one attribute to mutation function.

Values of vectorX and vectorY will be -1, 0, 1. We can send this values from method in component playground.

Vue.component('playground', {
    ...
    methods: {
        moveGameObject: function(gameObject, vectorX, vectorY) {
            store.commit("moveObject", { gameObject, vectorX, vectorY });
        },

This code here is pretty straightforward. It’s just transporting information about a move from component to the store. Now we need to tell HTML component to listen on click and send it to component. We will use v-on:click to send the event to JavaScript method mouseClicked. Note: @click is shorthand for v-on:click.

<script type="text/x-template" id="playground-template">
  <div @click="mouseClicked">

Now we’re missing just the last piece of the puzzle to make this thing work. It’s the method mouseClicked. This one will be a little bit tricky. Let’s split the method into several parts. In the first part, we will need to determine coordinates where a user clicked relative to the component. This requires some computation because the object of event does not contain the information about relative coordinates.

methods: {
        ...
        mouseClicked: function(event) {
            let rect = event.currentTarget.getBoundingClientRect();
            let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
            let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
            let top = rect.top + scrollTop;
            let left = rect.left + scrollLeft;

So we have the coordinates of click relative to the component. We would like to know whether a user clicked left, right, below or above the avatar. We need this information to determine in which direction avatar should move. We will retrieve coordinates of the first object (avatar) from the store. Then we will compute delta so we can decide the direction.

            let avatar = this.$store.state.gameObjects[0];
            let deltaX = (event.clientX - left - 32)/64 - avatar.x;
            let deltaY = (event.clientY - top - 32)/64 - avatar.y;

You may wonder why we’re dividing coordinates by 64. It’s size of tile from previous articles.

Based on delta we can decide what to do. First of all, it’s good to eliminate case when we should not do anything.

            // Ignore clicks within avatar box
            if (Math.abs(deltaX) < 0.5) {
                deltaX = 0;
            }

            if (Math.abs(deltaY) < 0.5) {
                deltaY = 0;
            }

           // Do not move in case of click inside the icon of avatar
            if ((deltaX === 0) && (deltaY === 0)) {
                return;
            }

Let’s add code for deciding in which direction should the main character move.

          if (Math.abs(deltaX) > Math.abs(deltaY)) {
                if (deltaX > 0) {
                    this.moveGameObject(avatar, 1, 0);
                } else {
                    this.moveGameObject(avatar, -1, 0);
                }
            } else {
                if (deltaY > 0) {
                    this.moveGameObject(avatar, 0, 1);
                } else {
                    this.moveGameObject(avatar, 0, -1);
                }
            }
        }

The result should look like this and it should react on mouse click:

The source code of this article is stored at Github branch article-04.

In the next article, we will add checks so the avatar does not move through other objects or walls.

You can find more HTML5 games at https://georgik.rocks/tag/games

Back to tutorial Sokoban in Vue.js

5. January 2019

How to write game with Vue.js – Sokoban – Part 3 – Display game objects

In previous article we were talking about graphics tiles. Now we would like to display game objects. Sokoban contains just two types of objects: avatar and boxes.

We will store these game objects in a list and we will use the same CSS trick to display graphic for each object like in the previous article. We will load just one PNG and define class for each tile that should be displayed.

Let’s define one avatar and one box in js/game.js:

const store = new Vuex.Store({
    state: {
        ...,
        gameObjects: [
            {
                name: 'avatar',
                x: 1,
                y: 1
            },
            {
                name: 'box',
                x: 2,
                y: 2
            }
        ]
    }
});

The template for HTML in index.html will iterate over the list. It will set coordinates using top and left CSS attributes. Graphics will be mapped by class name.

<script type="text/x-template" id="playground-template">
    <div>
    ...
    <div v-for="(gameObject, index) in gameObjects"
            :class="['game-object game-object-' + gameObject.name]"
            :style="{ top: gameObject.y*64 + 'px', left: gameObject.x*64 + 'px'}">
    </div>
    </div>
</script>

The last remaining thing is to define picture mapping in css/game.css from tiles.png.

.game-object {
    width: 64px;
    height: 64px;
    position: absolute;
    background: url("../data/images/gfx64/tiles.png") -192px -192px;
}

.game-object-avatar {
    background: url("../data/images/gfx64/tiles.png") -192px 0;
}

.game-object-box {
    background: url("../data/images/gfx64/tiles.png") -128px 0;
}

The result should look like this:

The source code of this article is stored at Github branch article-03.

In the next article, we will add controls to player’s avatar so it could move around the scene.

You can find more HTML5 games at https://georgik.rocks/tag/games

Back to tutorial Sokoban in Vue.js

1. January 2019

How to write game with Vue.js – Sokoban – Part 2 – Display level as graphics

In the previous article, we set the foundation for building a clone of Sokoban game. We displayed level in form of text. This form is good for ASCII-art fan. Let’s take a look at how to make it more user-friendly by replacing characters with graphic tiles.

We’re going to implement the following principle. Each tile will be represented by span with fixed height and width. Also it will have fixed coordinates which could be achieved by setting top and left attribute to proper value. Then we will use CSS trick to display graphics tile from one image using CSS background attribute.

Let’s start with CSS for our game in file css/game.css.

.tile {
    width: 64px;
    height: 64px;
    position: absolute;
    background: url("../data/images/gfx64/tiles.png") 0 0;
}

The trick with background URL will pick the 1st tile from tiles.png file which begins at coordinates 0 0.

Warning: Do not forget to use units in CSS definition. In our case it is px. When you define dimension without unit you may end up with very strange results.

Here is the image with tiles which we will use in our game.

Now we need to load CSS in index.html.

<link rel="stylesheet" type="text/css" href="css/game.css"/>

Let’s update the template for playground component so that tiles are displayed instead of letters.

    <div v-for="(tileRow, rowIndex) in levelMap">
        <span class="tile" v-for="(tileName, tileIndex) in tileRow"
            :style="{ top: rowIndex*64 + 'px', left: tileIndex*64 + 'px'}"></span>
    </div>

What is new in this template definition?

Clearly class=”tile” which will apply the style defined in game.css file to html element.

The second new thing is the definition of style. We can notice colon before word style. It’s shorthand for v-bind:style. This special syntax will tell Vue.js that the value of the attribute should be computed. In our case, we’re composing map which corresponds to syntax in CSS. If we look closely at the syntax it will render something like this:

{ top: 64px, left: 128px }

The question is how to map letter to from level map to tile stored in PNG file. The answer is simple. We will create a special class for each letter which references tile in PNG file. Then this class will be written into HTML code.

Let’s look at the first piece in css/game.css.

.tile {
...
    background: url("../data/images/gfx64/tiles.png") 0 0;
}

.tile-w {
    background: url("../data/images/gfx64/tiles.png") 0 -192px;
}

The pair of numbers indicates the start of tile from the top left corner. In CSS it’s recommended to use negative values for referencing coordinates to get the same experience across browsers.

Now, it’s necessary to update the code of template just a little bit.

        <span v-for="(tileName, tileIndex) in tileRow"
            :class="['tile tile-' + tileName]"
            :style="{ top: rowIndex*64 + 'px', left: tileIndex*64 + 'px'}"></span>

We’ve added :class which is abbreviation for v-bind:class. In this case we’re composing string which will be interpreted by web browser. So the result might look like: ’tile tile-w’

The result should look like this:

The source code of this article is stored at Github.

In the next article, we will add player’s avatar and box as game objects.

You can find more HTML5 games at https://georgik.rocks/tag/games

Back to tutorial Sokoban in Vue.js

30. December 2018

How to write game with Vue.js – Sokoban – Part 1 – Display level as text

Computer games have been written in nearly every computer technology. In 90' C and C++ were the kings. As we moved to the era of internet the number of available technologies exploed. Clearly, HTML and Javascript opened new horizons thanks to new and far simpler distribution. You just type an URL into a browser instead manual copying of floppy disks.

Let's take a closer look at Vue.js and how it can be used to write computer games. In this series, we will focus on building a clone of a very old game from 80': Sokoban.

Levels in Sokoban are fairly simple. The level consists of walls, boxes, spots where boxes should be moved and one character who is moving the stuff in a warehouse. The character can push just one box and he can't pull anything.

The following text will reference 4th commit on branch article-01 from repository https://github.com/georgik/pf2019/
You can access the version by commands:

git clone git@github.com:georgik/pf2019.git
cd pf2019 
git checkout acce8e7da89

Let's define one level where w stands for wall, o and x stands for objects in the game:

"wwwww",
"w w w",
"w oxw",
"wwwww"

Let's prepare the basic skeleton of an application so that we can display the map on the screen.

First of all, we're going to define references to Vue.js and Vuex libraries in index.html.

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.js"></script>

In the second step, we will define a template which will render our map into real HTML code.

<script type="text/x-template" id="playground-template">
    <div>
    <div v-for="(tileRow, rowIndex) in levelMap">
        <span v-for="(tileName, tileIndex) in tileRow">{{ tileName }}</span>
    </div>
    </div>
</script>

The template just iterates ower the levelMap. Vue.js has a construct for rendering lists v-for. The first attribute tileRow will contain one line of the array from levelMap. rowIndex will contain the index of the row which is being rendered. We do not need rowIndex right now, but it will be useful in the future for positioning of tiles.

The second nested v-for is iterating over characters in each array. In this implementation, we're going to display just the character. Later on, we will map it to graphics and that will require some CSS tricks.

Let's finish construction of index.html. We will need a tag where Vue.js will plug the application. It could be achieved by following code in body.

<div id="game">
    <playground></playground>
</div>

Div tag is clear. But playground is definitely not an HTML tag. Here will Vue.js plug the component that we prepare in Javascript.

Last, but not least is include of our js/game.js file where Javascript logic will be stored.

<script type="text/javascript" src="js/game.js"></script>

Now we will move to the second file js/games.js. First of all let's store our levelMap in Vuex.Store state. Just define it for now. We will explore Vuex later on.

const store = new Vuex.Store({
    state: {
        levelMap:  [
            "wwwww",
            "w w w",
            "w oxw",
            "wwwww"]
    }
});

Since we have a map we can define our component playground which we have seen in index.html.

Vue.component('playground', {
        template: '#playground-template',
        store,
        props: {},
        computed: {
            levelMap() {
                return this.$store.state.levelMap;
            }
        }
    });

Here you can see that the definition of playground component is binding together HTML template with id playground-template from index.html with playground HTML tag which will be supplied by attribute levelMap which will be read from Vuex store.

Seems little bit mindbending, take your time to understand the code.

The last missing piece to make this whole thing work is the definition of Vue application itself.

var game = new Vue({
  el: '#game',
  data: {}
});

The result should look like this:

The source code of this article is stored at Github.

In the next article, we will explain how to replace characters from a level map by tiles using CSS.

You can find more HTML5 games at https://georgik.rocks/tag/games

Back to tutorial Sokoban in Vue.js

28. December 2018

HTML5 Game: Sokoban – PF 2019

Sokoban written in Vue.js. Goal of the game: move lightbulbs and light them up.


Start the game in full screen mode

Source chode: GitHub

Technologies:

Artwork: CC OpenClips – Pixabay.com

Levels inspired by Borgar Þorsteinsson’s Sokoban

You can find more games at georgik.rocks/tag/games/