webfussel

blogfussel

kleiner dev blog von webfussel

Nice2Know - About shnaik

... and its development.

First things first: This is not a DevDiary kind of post. Those will follow later for other things!

Some time ago I decided I want to develop more stuff in my free time.
Most of the things I developed in the last months was for my work and as everyone should know…
At work there is no chance to be 100% creative when you have to be productive and make money somehow.

And this is all fine!

Anyways, I noticed a small decline in my skills - something that is absolutely not fine.

With a first small idea I wanted to get back into TypeScript development and pave the way for something bigger:
Snake! That awesome mobile game on the old Nokia smartphones.
Man, those where the times. Good mobile gaming. No micro transactions.
Just me, this stupid number layout on my phone and Snake… also Space Impact, but that doesn’t matter now.

So my goal was simple… Make Snake. I had to consider these steps:

  • A player character that moves on its own
  • You can only change directions to left/right relative to the head

And the main feature…

  • Grow when you eat!

Because I didn’t want to do all that canvas stuff on my own, I used a Library called Phaser 3 doing that for me.

I also didn’t want to waste time with these things because there is something more important:
The Game Logic itself which should be separate to a framework or library itself.

What I wanted to practice in general was TypeScript because I haven’t used it for quite some time even though I really like it.

So what I really want to talk about today is…

What exactly were the problems I faced?

Snake is a game simple enough to be done in a few hours.
I worked about 10 hours on it to get to the state it is now - so that was pretty fast.
The core game is finished - everything I’d do from now on would be nice to have.

What where the problems in the core game?

Moving the snake

The most basic concept in Snake: The movement. There are quite a few things to consider here:

  • Even though your snake starts with only a head it can only move left/right relative to its movement direction
  • The snake can grow as soon it ate something
  • Snake 2 introduced the feature of PacMan-Like go-through-wall-come-outside-on-the-other-side borders
    • Do I want to include this, or the classic die-on-collision feature?
    • I started a small comment poll for this on Twitter
    • Snake 1 won

I guess one thing is clear here: We should store all the parts of the snake in an array. Furthermore, I decided for myself it would be best to include a bit of Library logic into the player character.
So the array I created stored the drawn Rectangles with their individual positions and of course all in the same size.

So, how did I move the snake?

Some would say:
Okay, you have an array with all the parts of the snake. Just iterate over it and give every block the coordinates of block in front of it!

Good answer, right?
WRONG!

Why should I move the whole snake if only the first and last block change while moving?
The whole other body stays in its place!

So what I did:

  • Add a new block where you’re heading
  • Remove the tail…
    • … but only if the snake did not eat yet!

Why is the eating part important? It means I don’t have to write extra code to a new block when the snake ate - because I do it all the time.
When I always create a new head and delete the tail my snake stays the same size.
If I don’t delete the tail it automatically grows.

Here’s the whole player class I created (repository is linked at the end of the article):

Player.ts

import {Directions} from "../interfaces/Directions";
import {Coords} from "../interfaces/Coords";
import {MAIN_GAME_CONFIG} from '../GameConfig';
import Scene = Phaser.Scene;
import Rectangle = Phaser.GameObjects.Rectangle;

export default class Player {
    private blocks: Rectangle[] = [];
    private scene: Scene;
    private currentDirection: Directions;
    private nextDirection: Directions;
    private justEaten: boolean = false;
    private _isDed: boolean = false;
    get isDed(): boolean {
        return this._isDed;
    }
    set isDed(ded: boolean) {
        this._isDed = ded;
    }

    constructor(scene: Scene) {
        this.scene = scene;
        this.currentDirection = Directions.UP;
        this.nextDirection = Directions.UP;
    }

    eat() {
        this.justEaten = true;
    }

    addNewBlock(coords: Coords) {
        const block = this.scene.add.rectangle(coords.x, coords.y, MAIN_GAME_CONFIG.blockSize, MAIN_GAME_CONFIG.blockSize, 0xffffff);
        this.blocks.unshift(block);
    }

    getCurrentBlocks(): Rectangle[] {
        return this.blocks;
    }

    changeDirection(direction: Directions) {
        if (direction !== this.currentDirection) {
            if (this.currentDirection === Directions.UP || this.currentDirection === Directions.DOWN) {
                if (direction === Directions.LEFT || direction === Directions.RIGHT) {
                    this.nextDirection = direction;
                }
            } else if (this.currentDirection === Directions.LEFT || this.currentDirection === Directions.RIGHT) {
                if (direction === Directions.UP || direction === Directions.DOWN) {
                    this.nextDirection = direction;
                }
            }
        }
    }

    move() {
        this.currentDirection = this.nextDirection;
        const newCoords = {...this.blocks[0]};

        switch(this.currentDirection) {
            case Directions.UP:
                newCoords.y -= MAIN_GAME_CONFIG.blockSize;
                break;
            case Directions.DOWN:
                newCoords.y += MAIN_GAME_CONFIG.blockSize;
                break;
            case Directions.LEFT:
                newCoords.x -= MAIN_GAME_CONFIG.blockSize;
                break;
            case Directions.RIGHT:
                newCoords.x += MAIN_GAME_CONFIG.blockSize;
                break;
        }

        if (this.checkIfDead(newCoords)) {
            this._isDed = true;
            return;
        }

        this.addNewBlock(newCoords);

        if (!this.justEaten) {
            const deleteMe = this.blocks.pop();
            this.scene.children.remove(deleteMe);
        } else {
            this.justEaten = false;
        }
    }

    checkIfDead(nextCoords: Coords): boolean {
        if (nextCoords.x < MAIN_GAME_CONFIG.blockSize / 2 || nextCoords.x > MAIN_GAME_CONFIG.width() - MAIN_GAME_CONFIG.blockSize / 2
            || nextCoords.y < MAIN_GAME_CONFIG.blockSize / 2 || nextCoords.y > MAIN_GAME_CONFIG.height() - MAIN_GAME_CONFIG.blockSize / 2) {
            return true;
        }

        for (const block of this.getCurrentBlocks()) {
            if (nextCoords.x === block.x && nextCoords.y === block.y) {
                return true;
            }
        }

        return false;
    }
}

That was the main problem I faced in the beginning.
But there was a really stupid bug I found…

Crash into yourself backwards

If you changed your movement fast enough you could crash into yourself where you came from.
This only worked if your snake was at least 2 blocks long.

Why did that happen?
In the code above you see the already fixed version. At first if you pressed a key to move you changed the currentDirection of your snake. The movement always put a new block in the current direction you’re facing by then. So if you made a very quick double left move… the current direction faced into yourself.

I fixed this by adding a nextDirection which overrides currentDirection when moving.
So you can change your next direction as fast as you want - you cannot change it backwards into yourself,
because it checks with your current direction if you can even get this way.

That’s it

There weren’t any more problems.
If you can manage those things the rest will be super easy.
Just remember to use something like standardized coordinates.

Have fun!

Kategorien