Wait. What?

Yeah, you read the headline right. I've been talking with coders and I've noticed that a lot of us talk about how much better we are at learning by using something to build our own stuff. I've also interrogated a few people and come to the conclusion that we tend to follow tutorials very rigidly. It's largely the fault of those of us writing tutorials. We tend to just write down what were doing, like our readers are going to just be following along step-by-step.

This brings me back to my first programming book. This one:

Well, not this exact one, an earlier version of this book that actually had a different title (up til the 5th edition it was called Teach Yourself C in 21 Days). Anyway, that's not important. What is important is that this book repeatedly encouraged me to do more than just follow along. That's something I've held onto my whole life.

I've been dabling in HTML5/JS game programming, so I'm going to wade into a trio of tutorials written by Emanuele Feronato and available on http://www.emanueleferonato.com. I'm just going to illustrate my process here. At the end of this article I'm totally going to leave you hanging.

Starting Out

We're going to start with the tutorial linked below. It's really just an introduction and some heavily commented code.

http://www.emanueleferonato.com/2014/11/13/html5-swipe-controlled-sokoban-game-made-with-phaser/

Go ahead, take a look. I'll wait.

Done? Ok. Now here's my code. It's not as heavily commented, but my point should be pretty clear:

(() => {

    const Game = new Phaser.Game(320, 320, Phaser.AUTO, "", {preload: preload, create: create})

    const EMPTY = 0
    const WALL = 1
    const SPOT = 2
    const CRATE = 3
    const PLAYER = 4

    const UP = [0, -1]
    const DOWN = [0, 1]
    const LEFT = [-1, 0]
    const RIGHT = [1, 0]

    const TILES = 40

    const Levels = [
        [
            [WALL,WALL,WALL,WALL,WALL,WALL,WALL,WALL],
            [WALL,EMPTY,EMPTY,WALL,WALL,WALL,WALL,WALL],
            [WALL,EMPTY,EMPTY,WALL,WALL,WALL,WALL,WALL],
            [WALL,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,WALL],
            [WALL,WALL,EMPTY,SPOT,WALL,CRATE,EMPTY,WALL],
            [WALL,EMPTY,EMPTY,PLAYER,WALL,EMPTY,EMPTY,WALL],
            [WALL,EMPTY,EMPTY,EMPTY,WALL,WALL,WALL,WALL],
            [WALL,WALL,WALL,WALL,WALL,WALL,WALL,WALL]
        ]
    ]

    let level = 0

    let crates = []

    let player

    let startX
    let startY
    let endX
    let endY

    let fixedGroup
    let moveableGroup

    function preload() {
        Game.load.spritesheet("tiles", "tiles.png", TILES, TILES)
    }

    function create() {

        Game.scale.pageAlignHorizontally = true
        Game.scale.pageAlignVertically = true

        Game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL

        Game.scale.refresh(true)

        fixedGroup = Game.add.group()
        moveableGroup = Game.add.group()

        for (let y = 0; y < Levels[level].length; y++) {
            crates[y] = []
            for (let x = 0; x < Levels[level][y].length; x++) {
                crates[y][x] = null
                let currentTile = Levels[level][y][x]
                let tileX = x * TILES
                let tileY = y * TILES

                if ( currentTile === SPOT
                  || currentTile === SPOT+PLAYER
                  || currentTile === SPOT+CRATE
                  || currentTile === WALL) {
                    Game.add.sprite(tileX, tileY, "tiles", currentTile === WALL ? WALL : SPOT, fixedGroup)
                } else 
                
                if (currentTile === CRATE || currentTile === SPOT+CRATE) {
                    crates[y][x] = Game.add.sprite(tileX, tileY, "tiles", currentTile, moveableGroup)
                }

                if (currentTile === PLAYER || currentTile === SPOT+PLAYER) {
                    player = Game.add.sprite(tileX, tileY, "tiles", currentTile, moveableGroup)
                    player.posX = x
                    player.posY = y
                }

                if (currentTile === EMPTY || currentTile === CRATE || currentTile === PLAYER) {
                    Game.add.sprite(tileX, tileY, "tiles", EMPTY, fixedGroup)
                }

            }
        }

        Game.input.onDown.add(startSwipe, this)
    }

    function startSwipe() {
        startX = Game.input.worldX
        startY = Game.input.worldY
        Game.input.onDown.remove(startSwipe)
        Game.input.onUp.add(endSwipe)
    }

    function endSwipe() {
        endX = Game.input.worldX
        endY = Game.input.worldY

        let distX = endX-startX
        let distY = endY-startY

        if (distY < 0 && distY < -(Math.abs(distX)*2)) {
            movePlayer(UP)
        } else if (distY > 0 && distY > Math.abs(distX)*2) {
            movePlayer(DOWN)
        } else if (distX < 0 && distX < -(Math.abs(distY)*2)) {
            movePlayer(LEFT)
        } else if (distX > 0 && distX > Math.abs(distY)*2) {
            movePlayer(RIGHT)
        }

        Game.input.onUp.remove(endSwipe)
        Game.input.onDown.add(startSwipe)
    }

    function isClear(x, y) {
        return Levels[level][y][x] === EMPTY || Levels[level][y][x] === SPOT
    }
    
    function isCrate(x, y) {
        return Levels[level][y][x] === CRATE || Levels[level][y][x] === CRATE+SPOT
    }

    function movePlayer(deltas) {
        let dX = deltas[0]
        let dY = deltas[1]
        if (isCrate(player.posX+dX,player.posY+dY)) {
            let crateMoved = moveCrate(player.posX+dX, player.posY+dY, dX,dY)
            if (crateMoved === false) {
                return false
            }
        }
        if (!isClear(player.posX+dX,player.posY+dY)) {
            return false
        }

        let playerTween = Game.add.tween(player)
        
        playerTween.to({
            x: player.x + dX * TILES,
            y: player.y + dY * TILES
        }, 100, Phaser.Easing.Linear.None, true)

        Levels[level][player.posY][player.posX] -= PLAYER
        player.posX += dX
        player.posY += dY
        Levels[level][player.posY][player.posX] += PLAYER
        
        player.frame = Levels[level][player.posY][player.posX]
        
    }

    function moveCrate(sX, sY, dX, dY) {
        if (!isClear(sX+dX,sY+dY)) {
            return false
        }

        let crateTween = Game.add.tween(crates[sY][sX])
        crateTween.to({
            x: (sX+dX)*TILES,
            y: (sY+dY)*TILES
        }, 100, Phaser.Easing.Linear.None, true)

        Levels[level][sY][sX] -= CRATE
        Levels[level][sY+dY][sX+dX] += CRATE

        crates[sY+dY][sX+dX] = crates[sY][sX]
        crates[sY][sX].frame = Levels[level][sY+dY][sX+dX]
        crates[sY][sX] = null

    }

})()

Parts of my code resemble Emanuele's, but there are a lot of differences. No, I didn't just copy the code and start making changes until I liked the code style a bit better(hint: I still don't like the code above, but that's a topic for another article). I started at the top, looking at what each line/block/function was doing and reimplimenting it. Sometimes very similar, sometimes very different, but always my own way. That's the important part.

Sometimes it's easy to just copy and paste. You want to see something working. You want to quickly add some stuff to your GitHub to make it look more interesting. Maybe your RSIs are acting up and you just don't feel like typing that all out. Whatever the reason, you're missing the point. You may as well just be forking a repo. Hell, maybe that's exactly what you are doing. Whatever the reason and whatever the method, if your code looks just like the author's code, you're not learning very much, if you're learning anything at all.

Part 2

Moving on to part 2 of the tutorial series here:

http://www.emanueleferonato.com/2015/01/02/html5-swipe-controlled-sokoban-game-made-with-phaser-step-2-adding-keyboard-controls-and-unlimited-undos/

Here's where I really started to go off the trail. The author, again, just offers an introduction and a heavily commented chunk of source code. Reading the introduction, I know what my goal is. Add keyboard controls and an undo function. With that in mind, I start skimming through to see what's changing from the last tutorial to this one. I don't even read it all. Just looking for highlights. I quickly noticed one.

I didn't like how moveable sprites changed their sprite to match the floor they were moving onto before they moved onto it. The new code introduces Tween.onComplete. I don't even care what Emanuele is doing with it. I want to do something of my own with it. So I change my movePlayer and moveCrate functions like so:

        ...
        playerTween.to({
            x: player.x + dX * TILES,
            y: player.y + dY * TILES
        }, 100, Phaser.Easing.Linear.None, true)

        Levels[level][player.posY][player.posX] -= PLAYER
        player.posX += dX
        player.posY += dY
        Levels[level][player.posY][player.posX] += PLAYER
        
        playerTween.onComplete.add(function() {
            player.frame = Levels[level][player.posY][player.posX]
        })

Now my player sprite updates its frame after the tween runs instead of before. It still looks a little odd to my eyes, but I like it better and I've already done something the tutorial wanted me to do: create an onComplete callback to handle some game logic.

I notice that the author has broken out level drawing. I plan on adding more levels, so this seems like a good next step. I do that too.

Next, I get a quick look at Game.input.keyboard.addCallbacks. I look up the Game.input.keyboard documentation here and find out that Phaser has key value constants, so I can use Phaser.KeyMap.UP instead of 38 which you'll have already forgotten was the key code for the up arrow by the time you finish reading this sentence.

I also immediately get a bad taste in my mouth when I see a switch statement like the original author used. Don't get me wrong. I know that switch statements are useful, but I still feel like I've done something wrong somewhere in my code if I run into a situation where a switch is the best option. I know that I want to allow keyboard remapping, and the switch route won't work very well. I don't even finish reading Emanuele's keyboard code. I just start writing my own.

A dictionary of keys and actions and a keymap object that will get auto-populated later turning my key values into keys that can be used to directly access my actions. I'm hard-coding the keys here, I'll deal with remapping and the UI for that later.

    let keys = {
        "up": {
            key: Phaser.KeyCode.UP,
            action: () => { movePlayer(UP) }
        },
        "down": {
            key: Phaser.KeyCode.DOWN,
            action: () => { movePlayer(DOWN) }
        },
        "left": {
            key: Phaser.KeyCode.LEFT,
            action: () => { movePlayer(LEFT) }
        },
        "right": {
            key: Phaser.KeyCode.RIGHT,
            action: () => { movePlayer(RIGHT) }
        }
    }

    let keymap = {}

A function to convert my pretty, easy to read dict into a keymap that our code can easily use:

    function rebuildKeymap() {

        keymap = {}
        for (let k in keys) {
            if (!keys.hasOwnProperty(k)) {
                continue
            }

            keymap[keys[k].key] = keys[k].action
        }

    }

Finally, a very simple handler for our key-presses:

    function keyDown(e) {

        if (!keymap.hasOwnProperty(e.keyCode)) {
            return
        }

        keymap[e.keyCode]()

    }

It's a little more code than the original author used, but it's much better suited to what I want to do. Also, I had to think more about the code I was writing which helped me to learn what I was doing, much faster and better than if I'd just regurgitated what Emanuele wrote.

Closing Remarks

But the tutorial has three parts and we're not even through with part 2! Well, I did warn you I was going to do this.

If you read this far, congrats, you're good at paying attention. You may be missing the point though. Unless you skipped to here, in which case, you've totally got this thing of which this article is about figured out.

tl;dr

Don't follow tutorials and how-to articles like they're scripture or precise blueprints. Make the example project your own as you're making it.

Don't pad along dutifully in someone else's footprints.

Don't change it afterwards.

Look at each code fragment critically and examine how it fits into what you want to get out of the exercise, then do the exercise your own way.

Sources

My game code

Original Tutorials