{ phaser3 }

  • Sokoban Series: Part 5, Publishing your Game

    |

    After spending too many hours of your free time on creating a game you want people to play and enjoy it. Next to that, you also want to earn a bit of money from your game. CrazyGames has a Developer Portal that enables you to upload a game, and if there are no problems get it published within a couple of days.

    You automatically get part of the revenue that is made from advertisements on your game page, even without having any kind of advertisement in your game. You can increase the revenue share by letting CrazyGames have exclusive rights for the first couple of weeks of your game, by incorporating the logo in your game etc.
    The only minimum requirement for revenue share is that your game is not published anywhere else, and does not contain branding of a competing portal.

    It is also interesting for larger games that aim to release on Steam: you can submit an early version of your game, see what parts users like and dislike, while already earning some money. You can update your game after it has been released. And, you can choose to take down your game at any time; you remain the owner of your game.

    The upload process is fairly short. You fill in the basic information of your game, upload your game files, configure the revenue share options and you are done.

    game files

    revenue share

    You can immediately preview your game using using the Preview tool. This shows you what your game will approximately look like on CrazyGames. It also helps you to catch some final mistakes. You can update your game files at any point in time.

    preview

    Integrating Advertisements

    In case you want to increase the revenue from your game you can integrate advertisements into your game. CrazyGames provides an SDK that facilitates this process.

    The SDK comes with detailed instructions on how to install and use it.

    Installation

    Installing the SDK is done by including <script src="https://sdk.crazygames.com/crazygames-sdk-v1.js"></script> in your index.html. This installs the SDK under window.CrazyGames. The SDK requires that you use the game loader of CrazyGames: this is the loader that wraps your game, adds a full screen button, adds links to the Play store if your game is on it etc. It also contains the logic of requesting and displaying advertisements. The SDK comes with an HTML file that wraps your index.html. Just test your game by going to localhost/sdk.html instead of localhost/index.html.

    Setup of the SDK

    We instantiate the SDK in our init() function. The SDK is implemented using a singleton pattern, and you get an instance by calling getInstance().
    After this we make a call to sdk.init() which initializes the SDK. The SDK communicates through events to let you know the state of a requested advertisement.

    There are three kinds of events:

    • adStarted: fired when the advertisement starts playing. You should mute any music, and ensure that the game is not continuing playing
    • adFinished: fired when the advertisement finished playing. Resume your game.
    • adError: fired whenever some error occured, or when no advertisement is available. Continue with your game.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class LevelScene extends Phaser.Scene {
    // ads
    private sdk: any;
    private adRequested: boolean;

    init() {
    const { SDK } = (window as any).CrazyGames;
    this.sdk = SDK.getInstance();
    this.sdk.init();
    this.installListeners();
    }

    private installListeners() {
    this.sdk.addEventListener('adStarted', this.adStarted);
    this.sdk.addEventListener('adError', this.adError);
    this.sdk.addEventListener('adFinished', this.adFinished);
    }

    private adStarted = () => {
    this.sound.mute = true;
    }

    private adError = () => {
    this.sound.mute = false;
    this.adRequested = false;
    }

    private adFinished = () => {
    this.sound.mute = false;
    this.adRequested = false;
    }
    }

    Requesting an Advertisement

    We have installed the necessary event listeners, but still need to request an advertisement. We want to do this at the time where it annoys the user the least. We do it whenever we start a level. The SDK will automatically limit the number of advertisements that are displayed, so even when a user quickly finishes a level he won’t see to many advertisements.

    We place the call to requestAd() in the preload() function, so that the ad plays while other assets are loaded in the background. An advertisement is played
    as an overlay, so your game still runs in the background. As long as an advertisement is requested or playing we disable user interaction, simply by returning early from the update loop.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class LevelScene extends Phaser.Scene {
    preload() {
    this.requestAd();
    // load other assets if needed
    }

    update() {
    if (this.adRequested) {
    return;
    }
    // normal update logic here
    }

    private requestAd() {
    this.adRequested = true;
    this.sdk.requestAd();
    }
    }

    And that is all you need to do to get advertisements up and running.

    Sitelocking your Game

    Games can get copied and distributed to other platforms without your permission, and it is difficult to enforce to take down your game from a platform.
    What we can do is obfuscate our code and install a sitelock into the game that freezes your game when it runs on a disallowed domain.

    We will make use of obfuscator.io: they provide a specialized tool to obfuscate and sitelock your code, and there is no need to reinvent the wheel. They surely do a better job than we can do ourselves, and having an easy to remove sitelock check is almost useless. Don’t forget that people can always download your code, and just remove the sitelock code. We enable our game to run on the following domains: .crazygames.com, .gioca.re, .1001juegos.com, .onlinegame.co.id, .speelspelletjes.nl and localhost. These are all the domains affiliated with CrazyGames, and of course localhost so we can still test it locally.

    We install java-obfuscator as a dev-dependency (npm install -D javascript-obfuscator), and create a new build task in our package.json:

    1
    2
    3
    "scripts": {
    "obfuscate": "webpack --mode production && npx javascript-obfuscator build/bundle.js --output build/obfuscated.js --compact true --domain-lock '.crazygames.com, .gioca.re, .1001juegos.com, .onlinegame.co.id, .speelspelletjes.nl, localhost'"
    }

    This will build our game and then obfuscate our game bundle. One downside of obfuscating the code is that it can have an impact on performance. For sokoban this is not a problem as the game is not very resource intensive, but may be a problem for more complex games. We obfuscate everything: Phaser, React and our game. One solution would be to only obfuscate some of your game logic, and make a bundle using that code.

    You can always use the online interface on obfuscator.io to sitelock your game. Automating the task is easier though; you will surely update your game, apply some bug fixes or add new content. Automating the job costs a bit more time in the beginning, but quickly pays of. And of course make sure to upload the obfuscated code, and not the original file :).

    Conclusion

    We discussed how you can publish your game on CrazyGames, and how to integrate advertisement in between your levels to increase your revenue. Even if you do not do this last step you still get revenue from the advertisement on CrazyGames itself. Finally, we showed how you can protect your game from being stolen and being hosted without your permission.

    Here are the other parts of this series:

  • Sokoban Series: Part 4, Creating an Overlay in React

    |

    In this part of our Sokoban Series we will create an overlay for our game. The overlay contains our menu in which the user can select different levels, and a volume control. The end result looks like this:

    overlay

    We tried making a simple menu in Phaser, but we found that Phaser is not suited to do this in a straight-forward manner. It would take us a lot less work in simple HTML, so that’s what we did.

    Enter React and material-ui. React is a JS library to create user interfaces. Material-UI provide react components that implemnet Google’s Material Design, and that simply look a lot better than I can design myself. We will not explain React in this post, that could be a whole book on its own. We focus on how you can communicate with your Phaser game from outside of Phaser.

    So let’s hook React and Material-UI up to our Phaser game. Phaser itself is also just Javascript. So, we want to create a React app that renders a DOM, and in that DOM will be our Phaser game.

    We create a React Component that renders a container for our game, and that creates our game in componentDidMount(). componenDidMount is the earliest point in which we are sure that our game div exists.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import * as React from 'react';
    import { createGame } from '../core/game';

    class GameContainer extends React.Component<{}> {
    constructor(props: {}) {
    super(props);
    }

    componentDidMount() {
    createGame();
    }

    render() {
    return (
    <div id="game" />
    )
    }
    }

    export default GameContainer;

    The function createGame() simply calls the constructor of our Game class.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    export class Game extends Phaser.Game {
    constructor(conf: GameConfig) {
    super(conf);
    }
    }

    let game: Game;
    export function createGame() {
    game = new Game(config);
    }

    export function getGame(): Game {
    return game;
    }

    That’s all we had to do. Of course we must be careful that our GameContainer is always present in our React application, if not it will be unmounted, and our game would get destroyed. To this end, we render the overlay on top of the game. The game will still be available in the DOM, it will just be hidden behind an overlay.

    What are the components our overlay and menu needs:

    • A way to open the menu
    • A way to select and load a level
    • A way to control the volume

    Let’s start with the volume control. We want to have a component that, when clicked opens a slider. The slider indicates the current volume, and when slidered (yes, that’s a word from now on) it changes the volume of our game. As Phaser is simple JS, we can also call function from “outside” Phaser. By accessing our game we can
    simple access the sound.volume and modify it:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    import * as React from 'react';
    import { getGame } from '../core/game';

    import VolumeMuteIcon from '@material-ui/icons/VolumeMute';
    import VolumeDownIcon from '@material-ui/icons/VolumeDown';
    import VolumeUpIcon from '@material-ui/icons/VolumeUp';
    import VolumeOffIcon from '@material-ui/icons/VolumeOff';
    import IconButton from '@material-ui/core/IconButton';
    import Slider from '@material-ui/lab/Slider';
    import { Grid, Icon, ClickAwayListener } from '@material-ui/core';

    export interface VolumeControlState {
    isOpen: boolean;
    volume: number;
    isMuted: boolean;
    }

    export interface VolumeControlProps {

    }

    class VolumeControl extends React.Component<VolumeControlProps, VolumeControlState> {
    constructor(props: VolumeControlProps) {
    super(props);
    this.state = {
    volume: getGame().sound.volume,
    isMuted: getGame().sound.mute,
    isOpen: false,
    }
    }

    render() {
    const { isOpen } = this.state;
    return (
    <Grid container alignItems="center" style={{ minWidth: '200px' }} alignContent="flex-end">
    <Grid item>
    <IconButton onClick={this.iconClicked}>
    {this.renderIcon()}
    </IconButton>
    </Grid>
    {
    isOpen &&
    <Grid item style={{ flex: 1 }}>
    {this.renderSlider()}
    </Grid>
    }
    </Grid>
    );
    }

    private iconClicked = (event: React.MouseEvent) => {
    this.setState({
    isOpen: !this.state.isOpen,
    })
    }

    private unMute() {
    this.setState({
    isMuted: false,
    });
    getGame().sound.mute = false;
    }

    private renderSlider() {
    const { volume } = this.state;
    return (
    <Slider value={volume * 100} onChange={this.volumeChange} />
    )
    }

    private volumeChange = (event: React.ChangeEvent, value: number) => {
    const actualVolume = value / 100;
    this.setState({
    volume: actualVolume
    });
    this.unMute();
    getGame().sound.volume = actualVolume;
    }

    private renderIcon() {
    const { isMuted, volume } = this.state;
    if (isMuted) {
    return this.renderMuted();
    }
    if (volume === 0) {
    return this.renderVolumeOff();
    }
    if (volume < 0.5) {
    return this.renderLowVolume();
    }
    return this.renderVolume();
    }

    private renderMuted() {
    return <VolumeMuteIcon />;
    }

    private renderLowVolume() {
    return <VolumeDownIcon />;
    }

    private renderVolumeOff() {
    return <VolumeOffIcon />;
    }

    private renderVolume() {
    return <VolumeUpIcon />
    }
    }

    export default VolumeControl;

    Next, let’s create a LoadLevel component that renders a thumbnail of each level, and that when clicks loads that level. All of our levels are stored in a JSON that is loaded inside Phaser. We can also access the cache of Phaser from outside. Simply get the intance of your game and call .cache, and you can get all your loaded resources.

    The following component renders a grid with LevelTiles. The LevelTile itself simply renders a thumbnail of the level, the name of the level etc. You can export a thumbnail of a map in the Tiled map editor.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import * as React from 'react';
    import Grid from '@material-ui/core/Grid';
    import LevelTile from './LevelTile';
    import { getGame } from '../core/game';
    import sortLevels from '../core/utils/sortLevels';

    export interface LoadLevelProps {
    onSelected: (level: string) => void;
    }

    class LoadLevel extends React.Component<LoadLevelProps, {}> {
    constructor(props: LoadLevelProps) {
    super(props);
    }

    render() {
    return (
    <Grid container={true} spacing={8} justify="center">
    {this.renderLevelTiles()}
    </Grid>
    );
    }

    private renderLevelTiles() {
    const levels = getGame().cache.json.get('levels').levels;
    const sorted = sortLevels(levels);
    return sorted.map((level) => {
    return (
    <Grid item key={level.file}>
    <LevelTile level={level} onClick={this.onClick} />
    </Grid>
    );
    })
    }

    private onClick = (id: string) => {
    const { onSelected } = this.props;
    onSelected(id);
    }
    }

    export default LoadLevel;

    We now have the main components for our overlay. Let’s create our main menu that is rendered on top of our game:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    import * as React from 'react';
    import Toolbar from '@material-ui/core/Toolbar';
    import MenuIcon from '@material-ui/icons/Menu';
    import IconButton from '@material-ui/core/IconButton';
    import LoadLevel from './LoadLevel';
    import Dialog from '@material-ui/core/Dialog';
    import CloseIcon from '@material-ui/icons/Close';
    import Typography from '@material-ui/core/Typography';
    import { loadLevel, getGame } from '../core/game';
    import VolumeControl from './VolumeControl';
    import { Grid } from '@material-ui/core';

    export interface MenuState {
    isOpen: boolean;
    }

    class MainMenu extends React.Component<{}, MenuState> {
    constructor(props: {}) {
    super(props);
    this.state = {
    isOpen: false,
    }
    }

    render() {
    return (
    <div className="menu">
    <Toolbar>
    <Grid container alignItems="center">
    <Grid item xs={2}>
    {this.renderHamburger()}
    </Grid>
    <Grid item xs={10}>
    {this.renderVolume()}
    </Grid>
    </Grid>
    </Toolbar>
    {this.renderMenu()}
    </div>
    )
    }

    private renderHamburger() {
    return (
    <IconButton onClick={this.toggleOpen}>
    <MenuIcon />
    </IconButton>
    );
    }

    private renderVolume() {
    return <VolumeControl />;
    }

    private renderMenu() {
    const { isOpen } = this.state;
    return (
    <Dialog fullScreen={true} open={isOpen} onClose={this.close}>
    <Toolbar>
    <IconButton color="inherit" onClick={this.close} aria-label="Close">
    <CloseIcon />
    </IconButton>
    <Typography variant="title">Select Level</Typography>
    </Toolbar>
    <LoadLevel onSelected={this.levelSelected} />
    </Dialog>
    );
    }

    private toggleOpen = () => {
    this.setState({ isOpen: !this.state.isOpen });
    }

    private close = () => {
    this.setState({ isOpen: false });
    }

    private levelSelected = (level: string) => {
    this.close();
    loadLevel(level);
    }
    }

    export default MainMenu;

    The final part is ensuring that our Menu is rendered above the game. We use CSS to achieve this:

    1
    2
    3
    4
    5
    6
    .menu {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;
    }

    We enforce that our overlay is always rendered on a fixed position, and by using a higher z-index than our game it is rendered above our game.

    Conclusion

    In my opinion Phaser is ill suited to render complex interactive user interfaces. The DOM-tree is made specifically to create such interfaces, so I prefer to use that. In this episode we combined React with Material-UI to render a menu on top of our game. As Phaser is, in the end, simply Javascript interacting with your game from ‘the outside world’ is not that hard.

    This completes our game. The only thing left to do is to publish our game. We will show you how to publish your game on CrazyGames in the next episode.

  • Sokoban Series: Part 3, Handling Input and Game Logic

    |

    In this episode we will handle keyboard and touch input to move our player through the world. We will add the game logic that checks for valid moves and that verifies whether the win condition was met.

    Input

    We want to add two kinds of input: keyboard input for desktop users, and touch input for mobile users.

    We will start with keyboard input:

    Keyboard Input

    There are two ways to control the player character: either you move him around using the arrow keys, or alternatively you move around using WASD. French keyboards have an azerty layout, and would move around using ZQSD. As far as I know, there is no way to know the keyboard layout of the player. Luckily, as we only need these four keys, we can just let the player move forward using either Z or W, and move left using either Q or A. For other games this approach may not be viable.

    We create a function getPlayerDirection() that returns either the direction our player’s character must move to, or null when no key was touched. Here is the code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    export type PlayerDirection = 'playerLeft' | 'playerRight' | 'playerUp' | 'playerDown';

    class LevelScene extends Phaser.Scene {
    private leftKeys: Phaser.Input.Keyboard.Key[];
    private rightKeys: Phaser.Input.Keyboard.Key[];
    private upKeys: Phaser.Input.Keyboard.Key[];
    private downKeys: Phaser.Input.Keyboard.Key[];

    private createInputHandler() {
    this.leftKeys = [Phaser.Input.Keyboard.KeyCodes.LEFT, Phaser.Input.Keyboard.KeyCodes.A, Phaser.Input.Keyboard.KeyCodes.Q].map((key) => {
    return this.input.keyboard.addKey(key);
    });
    this.rightKeys = [Phaser.Input.Keyboard.KeyCodes.RIGHT, Phaser.Input.Keyboard.KeyCodes.D].map((key) => {
    return this.input.keyboard.addKey(key);
    });
    this.upKeys = [Phaser.Input.Keyboard.KeyCodes.UP, Phaser.Input.Keyboard.KeyCodes.W, Phaser.Input.Keyboard.KeyCodes.Z].map((key) => {
    return this.input.keyboard.addKey(key);
    });
    this.downKeys = [Phaser.Input.Keyboard.KeyCodes.DOWN, Phaser.Input.Keyboard.KeyCodes.S].map((key) => {
    return this.input.keyboard.addKey(key);
    });
    }

    private getPlayerDirection(): PlayerDirection | null {
    if (this.leftKeys.some((key) => key.isDown)) {
    return 'playerLeft';
    }
    if (this.rightKeys.some((key) => key.isDown)) {
    return 'playerRight';
    }

    if (this.upKeys.some((key) => key.isDown)) {
    return 'playerUp';
    }
    if (this.downKeys.some((key) => key.isDown)) {
    return 'playerDown';
    }
    return null;
    }
    }

    The handling of keyboard input is well documented in Phaser, and you can find plenty of examples on their example page. Mobile input is trickier, and will be discussed next.

    Touch Input

    For mobile we want the user to move the ingame character by tapping the screen. We have two options.

    • The first option is to move the character by tapping the sides of the scree. So when the user taps the top of the screen the player moves up, left to move left etc.
    • The second option was to have movement relative to the position of the player. So when the user taps to the left of the player’s character, he moves left.

    The first version of our game implemented the first option, but users preferred relative movement as it felt more natural. One of the parts that was difficult to get right with the first option was to find a cutoff that felt natural. What happens when a user presses the center of the screen? What happens when you press the topleft corner of the screen? In our opinion there is no correct answer, and it just shows that this option for movement is not the correct on. It was also hard to get the boundaries right for different screen sizes.

    This is the code for the first option:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class LevelScene extends Phaser.Scene {
    private getDirectionFromInput(currentTime: number): PlayerDirection {
    const bounds = this.physics.world.bounds;
    const pointer = this.input.pointer1;
    const isHorizontalCentered = pointer.x > bounds.right / 4 && pointer.x < bounds.right * 3 / 4;
    const isLeft = pointer.x < bounds.left + WIDTH / 3;
    const isRight = pointer.x > bounds.right - WIDTH / 4;
    const isTop = pointer.y < bounds.top + HEIGHT / 2;
    const isBottom = pointer.y > bounds.bottom - HEIGHT / 2;
    if (isLeft && isVerticalCentered) {
    return 'playerLeft';
    }
    if (isRight && isVerticalCentered) {
    return 'playerRight';
    }
    if (isTop && isHorizontalCentered) {
    return 'playerUp';
    }
    if (isBottom && isHorizontalCentered) {
    return 'playerDown';
    }
    return null;
    }
    }

    The better way is to have movement relative to the player position. We just need to compute the angle between the location of the player and the location of the pointer. You can use Phaser.Math.Angle to compute the angle between two points. We then move the player right when the angle is between +45 degrees and -45 degrees, up when it is between +45 and +135 degrees etc. To ensure we get angles between 0 and Math.PI * 2 you can use Phaser.Math.Angle.Normalize.

    There is still one tricky part. Remember that we centered the playing field in our canvas. Thus, when we ask the coordinates of our player, we get the in-game coordinate, but not the one that is rendered on screen. The touch input on the other hand returns on-screen coordinates. Luckily, you can use
    camera.getWorldPoint(x,y) to convert screen coordinates to world coordinates. A second mistake we made is that in classic mathematics the y-axis goes upwards, while the in-game y-axis goes down. A simple mistake that did cost us a head scratch :)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class LevelScene extends Phaser.Scene {
    private getDirectionFromInput(currentTime: number): PlayerDirection {
    const pointer = this.input.pointer1;

    if (!pointer.isDown) {
    return null;
    }

    // we want users to press for every movement, and not keep their finger down
    if (currentTime - pointer.downTime > 400) {
    return null;
    }

    const playerCenter = this.player.getCenter();
    const pointerWorld = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
    const angle = Phaser.Math.Angle.Normalize(Phaser.Math.Angle.Between(playerCenter.x, playerCenter.y, pointerWorld.x, pointerWorld.y, ));
    const quarter = Math.PI / 4;
    if (angle < quarter || angle >= (Math.PI + quarter * 3)) {
    return 'playerRight';
    }
    if (angle >= quarter && angle < quarter * 3) {
    return 'playerDown';
    }
    if (angle >= quarter * 3 && angle < quarter * 5) {
    return 'playerLeft';
    }
    if (angle >= quarter * 5 && angle < Math.PI + quarter * 3) {
    return 'playerUp';
    }
    return null;
    }
    }

    Game Logic

    Now that we have input we need to define the game logic. In theory we could do this using the physics system of Phaser: we can define collisions between the player and a crate, and let the player move the crates in this manner. The downside of this approach is that players need pixel perfect precision to place their crates, or they will become stuck behind walls.

    Given that the gameplay of Sokoban is so simple we can just implement everything ourselves without using the physics system.

    What are the things we need to do?

    • We need to check whether a square is free. A square is free when it is within bounds, does not contain a wall and does not contain a crate.
      This is needed to check whether our player can move to that location, or whether a crate can be moved to that location.
    • We need to check whether a crate can be moved to the next tile, and what the next tile is.
    • Do the actual moving of the player and the crate.

    So first let’s write a helper function getCrateAt(tileX, tileY) that returns the crate at the given position, or null when no crate is on that location. Keep in mind that our crates are represented using coordinates, while a tilemap refers to tile numbers. Luckily, you can simply use tileToWorldXY() to convert coordinates to a tile.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class LevelScene extends Phaser.Scene {
    private getCrateAt(tileX: number, tileY: number): Crate | null {
    const allCrates = this.crates.getChildren() as Crate[];
    const { x, y } = this.tileMap.tileToWorldXY(tileX, tileY);
    const crateAtLocation = allCrates.filter((crate) => {
    return crate.x === x && crate.y === y;
    });
    if (crateAtLocation.length === 0) {
    return null;
    }
    return crateAtLocation[0];
    }
    }

    Next, let’s write checkIfLocationIsFree(tileX, tileY):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class LevelScene extends Phaser.Scene {
    private checkIfLocationIsFree(tileX: number, tileY: number): boolean {
    if (tileX < 0 || tileX >= this.tileMap.width || tileY < 0 || tileY >= this.tileMap.height) {
    return false;
    }
    const hasWall = !!this.wallLayer.getTileAt(tileX, tileY);
    if (hasWall) {
    return false;
    }
    const crate = this.getCrateAt(tileX, tileY);
    return !crate;
    }
    }

    We need to check ‘the next tile’ at several loations:

    • Whenever a player moves we need to know what his target tile will be,
    • Whenever a crate is moved we need to to see whether the next location is free.

    To facilitate this we create getNextTile(startTile, direction, layer) that gets the next tile of startTile based on direction:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class LevelScene extends Phaser.Scene {
    private getNextTile(
    start: Phaser.Tilemaps.Tile,
    direction: PlayerDirection,
    layer: Phaser.Tilemaps.DynamicTilemapLayer | Phaser.Tilemaps.StaticTilemapLayer,
    ): Phaser.Tilemaps.Tile {
    const { x, y } = start;
    switch (direction) {
    case 'playerDown':
    return layer.getTileAt(x, y + 1);
    case 'playerUp':
    return layer.getTileAt(x, y - 1);
    case 'playerLeft':
    return layer.getTileAt(x - 1, y);
    case 'playerRight':
    return layer.getTileAt(x + 1, y);
    default:
    throw new Error('Unexpected direction');
    }
    }
    }

    Let’s add everything together, and create our movement logic:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    class LevelScene extends Phaser.Scene {
    private updatePlayer(direction: PlayerDirection | null) {
    if (!direction) {
    return;
    }
    this.troToMovePlayer(direction);
    }

    private troToMovePlayer(direction: PlayerDirection) {
    const { x, y } = this.player;
    const currentTile = this.tileMap.getTileAtWorldXY(x, y, true);
    const wall = this.getNextTile(currentTile, direction, this.wallLayer, true);
    if (!wall) { // out of bounds
    return;
    }
    if (wall.index > -1) {
    return;
    }
    const floor = this.getNextTile(currentTile, direction, this.floorLayer);
    const crate = this.getCrateAt(wall.x, wall.y);

    if (crate) {
    const oneFurther = this.getNextTile(wall, direction, this.floorLayer);
    if (!oneFurther.index < 0) { // out of bounds
    return false;
    }
    if (!this.checkIfLocationIsFree(oneFurther.x, oneFurther.y)) {
    return false;
    }
    }
    this.movePlayer(wall.x, wall.y);
    }

    private checkIfLocationIsFree(tileX: number, tileY: number): boolean {
    if (tileX < 0 || tileX >= this.tileMap.width || tileY < 0 || tileY >= this.tileMap.height) {
    return false;
    }
    const hasWall = !!this.wallLayer.getTileAt(tileX, tileY);
    if (hasWall) {
    return false;
    }
    const crate = this.getCrateAt(tileX, tileY);
    return !crate;
    }

    private getCrateAt(tileX: number, tileY: number): Crate | null {
    const allCrates = this.crates.getChildren() as Crate[];
    const { x, y } = this.tileMap.tileToWorldXY(tileX, tileY);
    const crateAtNewLocation = allCrates.filter((crate) => {
    return crate.x === x && crate.y === y;
    });
    if (crateAtNewLocation.length === 0) {
    return null;
    }
    return crateAtNewLocation[0];
    }

    private getNextTile(
    start: Phaser.Tilemaps.Tile,
    direction: PlayerDirection,
    layer: Phaser.Tilemaps.DynamicTilemapLayer | Phaser.Tilemaps.StaticTilemapLayer,
    ): Phaser.Tilemaps.Tile {
    const { x, y } = start;
    switch (direction) {
    case 'playerDown':
    return layer.getTileAt(x, y + 1);
    case 'playerUp':
    return layer.getTileAt(x, y - 1);
    case 'playerLeft':
    return layer.getTileAt(x - 1, y);
    case 'playerRight':
    return layer.getTileAt(x + 1, y);
    default:
    throw new Error('Unexpected direction');
    }
    }

    private movePlayer(tileX: number, tileY: number) {
    const { x, y } = this.tileMap.tileToWorldXY(tileX, tileY);
    const crate = this.getCrateAt(tileX, tileY);
    if (crate) {
    // not the prettiest way, we simply move the crate along the same direction
    // knowing that the next spot is available
    const crateX = crate.x + x - this.player.x;
    const crateY = crate.y + y - this.player.y;
    const newTile = this.floorLayer.getTileAtWorldXY(crateX, crateY);
    crate.moveTo(crateX, crateY, newTile);
    }
    this.player.moveTo(x, y);
    }
    }

    Next, we implement our Player moveTo(). We use animations to let our character move, and use a tween to move our player naturally. By using the onComplete handler we keep track of whether our playing is still moving or not.

    The code to move a crate is done in a similar fashion.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    class Player extends Phaser.GameObjects.Sprite {
    moveTo(newX: number, newY: number) {
    const direction = this.getDirection(newX, newY);
    if (!direction) {
    return;
    }
    this.state = 'moving';
    this.scene.tweens.add({
    targets: this,
    x: newX,
    y: newY,
    duration: GAME_SPEED,
    onStart: this.onMoveStart,
    onStartParams: [direction],
    onComplete: this.onMoveComplete,
    });
    }

    private onMoveStart = (tween: Phaser.Tweens.Tween, object: Phaser.GameObjects.GameObject, direction: PlayerDirection) => {
    this.state = 'moving';
    this.anims.play(direction);

    if (this.idleTimer) {
    this.idleTimer.destroy();
    }
    }

    private onMoveComplete = () => {
    this.state = 'standing';
    this.anims.stop();

    // in the final version we have undos and restarts that destroys our player
    // this check ensures that the callback doesnt crash
    if (!this.scene) {
    return;
    }

    // reset player to start frame after movement is complete
    this.idleTimer = this.scene.time.delayedCall(500, () => {
    this.setFrame(START_FRAME);
    this.idleTimer = null;
    }, [], this);
    }

    private getDirection(newX: number, newY: number): PlayerDirection | null {
    if (newX > this.x) {
    return 'playerRight';
    }
    if (newX < this.x) {
    return 'playerLeft';
    }
    if (newY > this.y) {
    return 'playerDown';
    }
    if (newY < this.y) {
    return 'playerUp';
    }
    return null;
    }

    Game Finished Logic

    The final functionality we need to implement is the check whether the game is complete. We check that every goal has a crate. However, we implemented this in an illogical fashion: instead of looping over all the goals we loop over all the crates, and ensure that the number of crates in the right position matches the number of goals. Looking back, looping over the goals is a lot more logical, but we never got around to rewrite it.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class LevelScene extends Phaser.Scene {
    private hasPlayerWon(): boolean {
    return this.allCratesInCorrectLocation();
    }

    private allCratesInCorrectLocation(): boolean {
    const crates = this.crates.getChildren() as Crate[];
    const inCorrectLocation = crates.filter((crate) => {
    const goal = this.goalLayer.getTileAtWorldXY(crate.x, crate.y);
    if (!goal) {
    return false;
    }
    return crate.onCorrectLocation(goal);
    });
    const goalTiles = this.getTiles((tile) => {
    return tile.index > 1;
    }, this.goalLayer);
    return inCorrectLocation.length === goalTiles.length;
    }
    }

    The crate.onCorrectLocation(goal) function simply checks whether the coordinates of the goal and the crate match.

    Conclusion

    We took our game with layers and sprites and added input handling. This enables us to move our player and crates in the game. We also added the logic of the game: we check whether crates can be moved, and whether the level is completed or not. Creating the game is now simply a matter of calling the right functions in your update loop, which we will leave up to you. In the next episode we will create a game menu using React on top of our game.

  • Sokoban Series: Part 2, Creating an Interactive Level

    |

    In this episode we will transform our tilemap from the previous episode into a map with different layers and sprites. This gives us a foundation to implement our game logic and to easily create new maps. Next to that, we will play around with the camera to render our map in the center of the canvas, so that maps with different sizes get rendered nicely.

    Object Classes

    I want to take a static definition of our game world, a tilemap, and convert it into something
    dynamic with sprites that we can play with. In sokoban we have the following objects:

    • Walls
    • Floor
    • Goals
    • Crates
    • Our hero

    The first three objects are static and will never change when playing, while the last two are dynamic. As the first three are static and don’t contain that much logic we can simply represent them using tiles and tilemaps. The dynamics ones can be represented using sprites. We discuss the latter two.

    Player Sprite

    Let’s open our editor and create a Player sprite. Most Phaser tutorials or documents that I have seen just use the base sprite class and modify its state externally. I find it nicer to have specialized classes for my objects that extends the sprite class.

    Create a sprites directory and player.ts inside that directory. For now our player won’t do much except be drawn on the screen. We’ll add animations later.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import * as Phaser from 'phaser';

    const START_FRAME = 'Player/player_05'; // depends on your tilesheet, check out the JSON

    class Player extends Phaser.GameObjects.Sprite {
    constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'assets', START_FRAME);
    }
    }

    export default Player;

    Our Player class doesn’t do much, besides calling the Sprite constructor with START_FRAME. This is the sprite that will be used to draw our player initially. Open assets.json to find the correct name.

    Crate Sprite

    Similar to our player we will create a crate sprite class as well:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const START_FRAME = 'Crates/crate_12';

    class Crate extends Phaser.GameObjects.Sprite {
    constructor(scene: Phaser.Scene, x: number, y: number, private crateType: number) {
    super(scene, x, y, 'assets', crateTypeToAsset(crateType));
    }
    }

    export default Crate;

    Creating our Level in Tiled

    In sokoban every square can only contain a single item: a square is either a floor, a goal, a wall, a crate or contains the player. In theory we could represent the whole map as a single layer. I prefer to have multiple layers, where each layer contains a single object type.

    Multiple layers also enable us to diversify the look of our level. With a single layer we would not be able to specify the looks of the floor under a crate. It also makes it a bit easier to get all items of a certain type, as they are all in a single layer.

    We create a floor, crates, goal, spawn and walls layer. The spawn layer contains a single sprite that indicates the starting position of the player. By making it a separate layer we can simply not render that layer in our game, but still encode the information in our map.

    tiled-layer

    Creating our Level in Phaser

    So, let’s import our level into Phaser. Instead of using our MainScene from earlier we’ll create a dedicated LevelScene.

    This is (part of) the code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    import * as Phaser from 'phaser';
    import Crate from '../sprites/crate';
    import Player from '../sprites/player';

    class LevelScene extends Phaser.Scene {

    private player: Player;
    private crates: Phaser.GameObjects.Group;

    // tilemap
    private tileSet: Phaser.Tilemaps.Tileset;
    private tileMap: Phaser.Tilemaps.Tilemap;

    // layers
    private floorLayer: Phaser.Tilemaps.DynamicTilemapLayer;
    private wallLayer: Phaser.Tilemaps.StaticTilemapLayer;
    private spawnLayer: Phaser.Tilemaps.StaticTilemapLayer;
    private goalLayer: Phaser.Tilemaps.StaticTilemapLayer;
    private crateLayer: Phaser.Tilemaps.StaticTilemapLayer;

    constructor() {
    super({
    key: 'LevelScene',
    });
    }

    preload() {
    this.load.tilemapTiledJSON('level01', `./assets/levels/level01.json`);
    }

    create() {
    this.tileMap = this.make.tilemap({ key: 'level01' });
    this.tileSet = this.tileMap.addTilesetImage('assets');
    this.createLevel();
    this.createPlayer();
    }

    private createLevel() {
    this.createLayers();
    this.createCrates();
    }

    private createLayers() {
    const x = 0;
    const y = 0;
    this.spawnLayer = this.tileMap.createStaticLayer('Spawns', this.tileSet, x, y);
    this.spawnLayer.setVisible(false);
    this.crateLayer = this.tileMap.createStaticLayer('Crates', this.tileSet, x, y);
    this.crateLayer.setVisible(false);
    this.floorLayer = this.tileMap.createStaticLayer('Floors', this.tileSet, x, y);
    this.wallLayer = this.tileMap.createStaticLayer('Walls', this.tileSet, x, y);
    this.goalLayer = this.tileMap.createStaticLayer('Goals', this.tileSet, x, y);
    }

    private createCrates() {
    const crateTiles = this.getTiles((tile) => {
    return tile.index > -1;
    }, this.crateLayer);
    const crateSprites = crateTiles.map((tile) => {
    const { x, y } = this.tileMap.tileToWorldXY(tile.x, tile.y);
    const { type } = tile.properties as { type: number };
    const crate = new Crate(this, x, y, type);
    this.add.existing(crate);
    return crate;
    });
    this.crates = this.add.group(crateSprites);
    }

    private createPlayer() {
    const playerSpawn = this.getSpawn();
    const { x, y } = this.tileMap.tileToWorldXY(playerSpawn.x, playerSpawn.y);
    this.player = new Player(this, x, y);
    this.add.existing(this.player);
    }
    }

    export default LevelScene;

    That’s a lot. Let’s break it down. In our preload function we load our tiled map data, and in our create funcion we’ll create the tile map. This is similar to our previous example. We turn our spawn and crate layer invisible. We don’t want to render our spawn, we just need it to know where our player starts. Our crates won’t be represented by tiles. Instead, we will use the Crate sprite we created earlier.

    The function createCrates will create all the crates. It uses a helper function getTiles to filter all the tiles of certain layer. We’ll find all the tiles of the crateLayer that have a positive index: Phaser either represents a non-existing tile as null, or can provide a stub tile with a negative index (but that still has an x and y coordinate etc.). We take all the tiles that have a crate, and create an instance of a Crate sprite. These are added to the Crate group. Note that crates use world coordinates, while tiles use tile map coordinates. World coordinates are the actual coordinates that are used to draw on the screen. Tile map coordinates are better suited for tile maps: the topleft tile has coordinate (0,0), the tile next to that (1,0) etc.

    We create our player in a similar way.

    So, how does getTiles and getSpawn work? Here is the code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class LevelScene {
    ...
    private getSpawn() {
    const spawns = this.getTiles((tile) => {
    return tile.index > - 1
    }, this.spawnLayer);
    if (spawns.length !== 1) {
    throw new Error(`[LevelScene] Expected single spawn`);
    }
    return spawns[0];
    }

    private getTiles(
    test: (tile: Phaser.Tilemaps.Tile) => boolean,
    layer?: Phaser.Tilemaps.StaticTilemapLayer
    ): Phaser.Tilemaps.Tile[] {
    this.tileMap.setLayer(layer || this.floorLayer);
    return this.tileMap.filterTiles((tile: Phaser.Tilemaps.Tile) => {
    return test(tile);
    }, this, 0, 0, this.tileMap.width, this.tileMap.height);
    }
    }

    getTiles uses filterTiles to filter out all the tiles of a tilemap. A tilemap has a current layer against which all operations run. So, we first set the active layer to the given layer (or the floor layer). filterTiles allows you to define a rectangle on which the filter operates. We always want to full tile map, so we start in the topleft corner (0,0), and have a rectangle with width tileMap.width and height tileMap.height.

    Next I want to make the distinction between tiles clearer, so it is easier for our players to navigate the gridlike map. We draw a horizontal and vertical grid with dotted lines. We create a generic function addGridLine that can draw horizontal and vertical dotted lines, and use this function to draw our grid.

    Here is the code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    class Level {
    ...
    private createGridLines() {
    const lineLength = 6;
    const skipLength = 2;
    this.addVerticalLines(lineLength, skipLength);
    this.addHorizontalLines(lineLength, skipLength);
    }

    private addVerticalLines(lineLength: number, skipLength: number) {
    let currentX = 0;
    const stopX = this.tileMap.widthInPixels;
    const stopY = this.tileMap.heightInPixels;
    const next = (x: number, y: number) => ({ x: x, y: y + lineLength });
    const skip = (x: number, y: number) => ({ x: x, y: y + skipLength });
    const stop = (x: number, y: number) => y >= stopY;
    while (currentX <= stopX) {
    this.addGridLine(currentX, 0, next, skip, stop);
    currentX += this.tileMap.tileWidth;
    }
    }

    private addHorizontalLines(lineLength: number, skipLength: number) {
    let currentY = 0;
    const stopX = this.tileMap.widthInPixels;
    const stopY = this.tileMap.heightInPixels;
    const next = (x: number, y: number) => ({ x: x + lineLength, y: y });
    const skip = (x: number, y: number) => ({ x: x + skipLength, y: y });
    const stop = (x: number, y: number) => x >= stopX;
    while (currentY <= stopY) {
    this.addGridLine(0, currentY, next, skip, stop);
    currentY += this.tileMap.tileHeight;
    }
    }

    private addGridLine(
    startX: number,
    startY: number,
    next: (x: number, y: number) => { x: number, y: number },
    skip: (x: number, y: number) => { x: number, y: number },
    stop: (x: number, y: number) => boolean,
    ) {
    let currentX = startX;
    let currentY = startY;
    const line = this.add.graphics({
    x: 0, y: 0,
    lineStyle: { width: 1, alpha: 0.5, color: 0x000000 },
    fillStyle: { color: 0x000000, alpha: 1 },
    });
    line.beginPath();
    line.moveTo(startX, startY);
    while (!stop(currentX, currentY)) {
    const { x: nextX, y: nextY } = next(currentX, currentY);
    line.lineTo(nextX, nextY);
    const { x: skipX, y: skipY } = skip(nextX, nextY);
    line.moveTo(skipX, skipY);
    currentX = skipX;
    currentY = skipY;
    }
    line.closePath();
    line.strokePath();
    line.fillPath();
    }
    }

    Centering our Map

    We currently draw our map in the top left corner of our canvas. It would be nicer if our map would be drawn in the center, regardless of its size. We can do this using the Phaser 3 camera system, which got a nice revamp. I’ll be honest that it took me a while to get this working, and I noticed that on older versions of chrome this doesn’t work properly.

    So, this code renders our game in the center of the canvas:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class LevelScene {
    private centerCamera() {
    const bounds = this.physics.world.bounds;
    const x = bounds.width / 2 - this.tileMap.widthInPixels / 2;
    const y = bounds.height / 2 - this.tileMap.heightInPixels / 2;
    this.cameras.remove(this.cameras.main);
    const camera = this.cameras.add(x, y, this.tileMap.widthInPixels, this.tileMap.heightInPixels, true);
    camera.setOrigin(0, 0);
    camera.setScroll(0, 0);
    }
    }

    It works by taking the bounds of our world, in other terms the size of our canvas. We compute the center point, and then offset that point with the half of our tilemap. x and y point to the top left corner of where we want to draw our tile map.

    Next, we remove the main camera. Don’t ask me why. If I simply updated the main camera the code didn’t work. It’s probably a mistake on my part, but I just moved on. We can’t simply remove the main camera without adding a new main camera, so we do that. We create a camera that projects onto x and y with the width and height of our map, but which ‘looks’ at the topleft corner. So, we can just keep on using (0,0) as the topleft corner of our game, while the camera renders everything in the center.

    In the final version I added some text (“Level Complete”) that grows when you complete a level. The text was cut of on the left and right side as the growing effect fell outside the viewport of our camera. To solve that we simply let the camera render from -100 with a width of widthInPixels + 200, and set the scroll to (-100, 0).

    Conclusion

    In this episode we converted our plain tilemap into different layers and sprites, and centered our game into the middle of the camera. Our sprites don’t do much… yet. That is for the next episode.

  • Sokoban Series: Part 1, Getting Everything Up and Running

    |

    Sokoban

    In this series we discuss our Sokoban clone, written in Phaser 3. The game is available on CrazyGames.

    For those that are not familiar with Phaser, it is a nice framework to develop (mainly 2D) HTML5 games. Phaser 3 has been in development for some time now, and has been deemed stable for a couple of months.

    Sokoban is an old Japanese puzzle game from the 80s. Your character is a warehouse employee that needs to push crates into the correct location. You can only push a single crate at the time, and must be careful not to get stuck.

    The focus of this writeup lies on the implementation, and we’ll show a lot of code snippets. The reason for this is that, while the implementation of Phaser 3 itself may be done, it is still lacking some documentation and more advanced code examples. Nonetheless, the documentation and examples pages are invaluable resources, and something I always had open during development. We try to highlight some of the issues we encountered while developing Sokoban, and how we solved them.

    Some of the details of our implementation are left out to increase readability, and to narrow the discussion to Phaser itself. It is possible that some inconsistenties can be found in these snippets due to this cleanup.

    Table of Contents

    This is a multipart series, where we discuss the following topics:

    This post discusses the first part: Getting Phaser & Typescript up and running.

    Getting Started

    In this section we discuss how to get Phaser 3 up and running with Typescript, and get a nice development environment where we can test our game during development.

    Getting Typescript up and Running

    We start by getting Phaser 3 up and running with Typescript. Typescript is a typed version of Javascript that compiles down to Javascript. I prefer working with Typescript as you get features such as tab completion, type information which is very handy with a large framework such as Phaser 3, and potential errors get caught early by your compiler.

    Phaser 3 has support for Typescript, even though the type definitions do not cover Phaser 100%. Type definitions state what arguments a certain function takes, and what the return type is. Instead of setting up everything ourselves we can start from this skeleton project.

    We clone it, rename the folder to Sokoban, remove the .git folder and import it into our code editor. In our case this is Visual Studio Code. Next, we edit the package.json file, and ensure we’re using the latest version of Phaser 3. You can see the latest version by going to npmjs.com. Installing all the dependencies is done by running npm install.

    We replace the phaser.d.ts typings file with the latest version found here. We move everything from the src/boilerplate directory into the src/ directory, and remove everything else. Remove the comment at the top of game.ts that refers to the Phaser typings. Typescript should detect the typings without any assistance. Finally, we edit webpack.config.js so that entry points to ./src/game/ts.

    When you try to run your project (npm run dev) you should get redirected to your browser and greeted by your game. It’s possible you get some warnings, or you have to remove some additional comments in game.ts. You can developer your game while running this command, and your game will automatically refresh when it is modified.

    Getting Assets

    Before we can start developing we need some assets we can work with. I prefer not to spend too much time on creating my own assets, at least not in the beginning of a game. I bought the Kenney Bundle, which includes assets for Sokoban. The assets are better than any asset I can produce myself, and I prefer to program over editing in GIMP or Photoshop. But feel free to create your own assets. In the beginning you can even start with simple colored squares that represent in-game objects.

    In order to import the assets into Phaser we create a sprite sheet. A sprite sheet turns all a set of images into one big image, and tells Phaser how to divide the big image back into the indivual assets. The advantage is that you only need to import one image instead of hundred small images. To this end, we use Texture Packer. They have a nice tutorial how to create spritesheets. Add a smart folder with the Sokoban assets, and export them as a Phaser (JSONHash). I prefer to select trim sprite names, which removes the extension of your individual assets. This means you do not need to worry whether your asset was a .jpg or a .png, or you can even replace them with a different format.

    texture packer

    Creating a Tile Map

    Our Sokoban level can be represented by a tile map. Tile maps are perfect for topdown maps for many oldschool games such as Bomberman, Pacman, and also Sokoban. Tiled is a free tile map editor that is easy to use and comes with a wide series of features. Install Tiled, and create a new tile map. Next, import our sprite sheet that we previously created. Make sure to select “embed spritesheet” and not save it as a separate file. Finally, draw a map and export it as a json file. Save everything into your assets directory of our project.

    tiled image

    Getting Everything in Phaser

    Let’s try to get everything in Phaser. We start by cleaning up the last parts of the skeleton project. Remove everything in assets (except for our own stuff). Then let’s import our map. Let’s edit mainScene.ts:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export class MainScene extends Phaser.Scene {
    private tileSet: Phaser.Tilemaps.Tileset;
    private tileMap: Phaser.Tilemaps.Tilemap;

    constructor() {
    super({
    key: "MainScene"
    });
    }

    preload(): void {
    this.load.atlas('assets', './assets/assets.png', './assets/assets.json');
    this.load.tilemapTiledJSON('level', `./assets/levels/level.json`);
    }

    create(): void {
    this.tileMap = this.make.tilemap({ key: 'level' });
    this.tileSet = this.tileMap.addTilesetImage('assets');
    this.tileMap.createStaticLayer('Tile Layer 1', this.tileSet, x, y);
    }
    }

    In our preload() function we load our spritesheet and tiled map. In the create() function we create the actual objects. When we start our application again we should see our tilemap, although not much interesting will be happening.

    Conclusion

    In this episode we installed and set up Phaser 3 with Typescript. We used an available template project that handles most of the setup for us. Next, we created (well… bought) and imported assets that we will use for the rest of our game. In the next episode we will convert our tilemap into actual game objects that can move.