Skip to content

Creating a Game

In the Quick Start guide, we learned how to set up a very simple project using dill pixel. Now let’s go through the process of creating a basic game.

Setup the Files

  1. First, create a package.json in your project’s root directory:

    package.json
    {
    "name": "my-dill-pixel-game",
    "version": "0.0.0",
    "description": "My First Game with dill pixel",
    "main": "index.html",
    "type": "module",
    "scripts": {
    "start": "vite",
    "dev": "vite",
    "build": "vite build"
    },
    "author": "Me<me@myname.com>",
    "dependencies": {
    "dill-pixel": "^3.1.9",
    "typescript": "^5",
    "vite": "^5",
    "vite-plugin-static-copy": "^1"
    }
    }
  2. Create a vite.config.js file in the root directory to configure the build process. The default Vite configuration is sufficient but for handling static assets we’re using the vite-plugin-static-copy so that they can be loaded from within the game. You can configure the plugin however you like based on how you’ve organized your assets.

    vite.config.js
    import path from 'path';
    import { fileURLToPath } from 'url';
    import { defineConfig, normalizePath } from 'vite';
    import { viteStaticCopy } from 'vite-plugin-static-copy';
    const __dirname = fileURLToPath(new URL('.', import.meta.url));
    /** @type {import('vite').UserConfig} */
    export default defineConfig((config) => ({
    ...config,
    target: 'esnext',
    logLevel: 'info',
    plugins: [
    viteStaticCopy({
    watch: { reloadPageOnChange: true },
    targets: [
    {
    src: normalizePath(path.resolve(__dirname, './src/assets/images/spritesheets/_output/*')),
    dest: './assets/images/spritesheets',
    },
    {
    src: normalizePath(path.resolve(__dirname, './src/assets/images/static/**/*')),
    dest: './assets/images/static',
    },
    {
    src: normalizePath(path.resolve(__dirname, './src/assets/json/*')),
    dest: './assets/json',
    },
    {
    src: normalizePath(path.resolve(__dirname, './src/assets/spine/*')),
    dest: './assets/spine',
    },
    {
    src: normalizePath(path.resolve(__dirname, './src/assets/fonts/*')),
    dest: './assets/fonts',
    },
    {
    src: normalizePath(path.resolve(__dirname, './src/assets/audio/output/*')),
    dest: './assets/audio',
    },
    ],
    }),
    ],
    resolve: {
    alias: {
    '@': path.resolve(__dirname, './src'),
    },
    },
    }));
  3. Next, create an index.html file in the root directory. This file will be the entry point for your game and dill pixel will automatically create a <div id="game-container" /> to contain the game canvas:

    index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <title>My dill pixel Game</title>
    </head>
    <body>
    <script type="module" src="src/index.ts" async defer></script>
    </body>
    </html>
  4. Create an index.css file in the src directory that displays the game at full screen:

    src/index.css
    :root {
    --app-width: 100dvw;
    --app-height: 100dvh;
    }
    body, html {
    position: fixed;
    margin: 0;
    padding: 0;
    width: var(--app-width, 100%);
    height: var(--app-height, 100%);
    }
    #game-container {
    width: 100%;
    height: 100%;
    overflow: hidden;
    }
  5. Create an index.ts file in the src directory that initializes the game:

    src/index.ts
    import { create } from 'dill-pixel';
    import { Application } from './Application';
    import './index.css';
    // Create the application
    create(Application, {
    antialias: false,
    resizeOptions: {
    minSize: { width: 375, height: 700 },
    },
    showStateDebugMenu: true,
    resolution: Math.max(window.devicePixelRatio, 2),
    backgroundColor: 0x0,
    });

Create the Game States

Loading Screen

Create a state folder in the src directory and create a LoadScreen.ts file in it. This will be displayed while the game is loading and also when transitioning between states.

To manage the transitions smoothly, we’re using gsap to fade the loading screen in and out in the animateIn() and animateOut() methods:

src/state/LoadScreen.ts
import { LoadScreen as DillPixelLoadScreen } from 'dill-pixel';
import { gsap } from 'gsap';
import { Sprite, Point } from 'pixi.js';
// The load screen is simply a black background
export class LoadScreen extends DillPixelLoadScreen {
public static NAME: string = 'LoadScreen';
private _bg!: Sprite;
// Create the black background
public init(pSize: Point) {
super.init(pSize);
this._bg = this.add.coloredSprite(0x000000);
}
// Animate the black background in from alpha 0 to 1
public async animateIn(pOnComplete: () => void): Promise<void> {
await gsap.timeline().fromTo(
this._bg,
{
alpha: 0,
},
{
duration: 0.5,
alpha: 1,
},
);
pOnComplete();
}
// Animate the black background out from alpha 1 to 0
public async animateOut(pOnComplete: () => void): Promise<void> {
await gsap.timeline().fromTo(
this._bg,
{
alpha: 1,
},
{
duration: 0.5,
alpha: 0,
},
);
pOnComplete();
}
// When the screen resizes, resize the background
public onResize(pSize: Point) {
super.onResize(pSize);
if (this._bg) {
this._bg.width = this._size.x;
this._bg.height = this._size.y;
}
}
}

Intro Screen

Create an IntroScreen.ts file in the state folder. This will be displayed when the game starts. Similar to the LoadScreen, there are animateIn() and animateOut() methods to control how the screen is revealed and hidden. For simplicity’s sake we’re just creating a green background and adding a title that can be clicked to take us to the next screen:

src/state/IntroScreen.ts
import { State, FlexContainer } from 'dill-pixel';
import { Point, Sprite, Text } from 'pixi.js';
import { Application } from '../Application';
// The intro screen is a green background with a centered title
export class IntroScreen extends State<Application> {
static NAME: string = 'IntroScreen';
protected _bg: Sprite;
protected _layout: FlexContainer;
protected _title: Text;
public constructor() {
super();
}
// Create the elements
public init(pSize: Point) {
super.init(pSize);
// Create the green background
this._bg = this.add.coloredSprite(0x33aa66);
this._bg.eventMode = 'static';
// Use a flex container for the layout
this._layout = this.add.flexContainer({
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 20,
});
// Add the title text
this._title = this._layout.add.text({
value: 'Click me to start',
style: {
fontFamily: 'sans-serif',
fontSize: 36,
fill: 'white',
},
anchor: 0.5,
});
// Make the title clickable
this._title.eventMode = 'static';
this._title.cursor = 'pointer';
// When the title is clicked, transition to the game screen
this._title.on('pointerdown', () => {
this.app.state.transitionTo('GameScreen');
});
this.onResize(pSize);
}
public async animateIn(pOnComplete: () => void): Promise<void> {
pOnComplete();
}
public async animateOut(pOnComplete: () => void): Promise<void> {
pOnComplete();
}
// When the screen resizes, resize the background
public onResize(pSize: Point) {
super.onResize(pSize);
if (this._bg) {
this._bg.width = this._size.x;
this._bg.height = this._size.y;
}
}
// Clean up
public destroy() {
super.destroy();
}
}

Game Screen

Create a GameScreen.ts file in the state folder. This will be the main game screen with a few elements: an image, a title, a message, and a button.

Clicking on the image will trigger a short animation and increment a counter. Clicking the button will take us back to the IntroScreen.

Note that in the destroy() method we’re cleaning up the gsap animations.

src/state/GameScreen.ts
import { State, Container, FlexContainer, AssetMapData, TextureAsset, AssetType } from 'dill-pixel';
import { Sprite, Text, Point } from 'pixi.js';
import gsap from 'gsap';
import { Application } from '../Application';
// This screen displays a rose background with a pickle, a title, and a button to go back to the intro screen
export class GameScreen extends State<Application> {
public static NAME: string = 'GameScreen';
protected _bg: Sprite;
protected _layout: FlexContainer;
protected _pickle: Sprite;
protected _title: Text;
protected _message: Text;
protected _button: Container;
protected _count: number = 0;
public constructor() {
super();
}
// Register the assets that are needed for this screen
public static get Assets(): AssetMapData[] {
return [new TextureAsset('pickle', AssetType.PNG)];
}
public init(pSize: Point) {
super.init(pSize);
// Create the background
this._bg = this.add.coloredSprite(0xaa5566);
this._bg.eventMode = 'static';
// Use a flex container to layout the elements
this._layout = this.add.flexContainer({
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: 20,
});
// Add the pickle image and make it clickable
this._pickle = this._layout.add.sprite({ asset: 'pickle' });
this._pickle.eventMode = 'static';
this._pickle.cursor = 'pointer';
this._pickle.on('pointerdown', async (e) => {
// Increment the count and update the message
this._count++;
this._message.text = `You clicked the pickle ${this._count} times!`;
// Animate the pickle
await gsap.timeline().fromTo(
this._pickle,
{
y: '0',
},
{
y: '-=100',
duration: 0.5,
ease: 'power2.out',
}
)
.to(this._pickle, {
y: '0',
duration: 0.75,
ease: 'bounce.out',
});
});
// Add the title
this._title = this._layout.add.text({
value: 'Welcome to the game',
style: {
fontFamily: 'sans-serif',
fontSize: 36,
fill: 'white',
},
anchor: 0.5
});
// Add the message
this._message = this._layout.add.text({
value: 'Click the pickle!',
style: {
fontFamily: 'sans-serif',
fontSize: 24,
fill: 'black',
},
anchor: 0.5
});
// Add the button to go back to the intro screen
this._button = this._layout.add.container({
alpha: 1,
position: [0,40],
});
this._button.add.coloredSprite({ color: 0xffffff, size: [200, 60], shape: 'rounded_rectangle', radius: 10 });
this._button.add.text({ value: 'Go back', anchor: 0.5 });
this._button.eventMode = 'static';
this._button.cursor = 'pointer';
this._button.on('pointerdown', (e) => {
this.app.state.transitionTo('IntroScreen');
});
this.onResize(pSize);
}
public async animateIn(pOnComplete: () => void): Promise<void> {
pOnComplete();
}
public async animateOut(pOnComplete: () => void): Promise<void> {
pOnComplete();
}
// When the screen resizes, resize the background
public onResize(pSize: Point) {
super.onResize(pSize);
if (this._bg) {
this._bg.width = this._size.x;
this._bg.height = this._size.y;
}
}
// Clean up
public destroy() {
super.destroy();
gsap.killTweensOf(this._pickle);
}
}

Create the Application

Create an Application.ts file in the src directory. This is the main application class that will manage the game states and transitions between them:

src/Application.ts
import { Application as DillPixelApplication, TransitionType } from 'dill-pixel';
// Load all of the game states
import { LoadScreen } from './state/LoadScreen';
import { IntroScreen } from './state/IntroScreen';
import { GameScreen } from './state/GameScreen';
export class Application extends DillPixelApplication {
// Register the load screen and define the transition type between states
protected setup() {
(globalThis as any).__PIXI_APP__ = this;
this.registerDefaultLoadScreen(LoadScreen);
this.state.defaultTransitionType = TransitionType.TRANSITION_SIMPLE_INTERSTITIAL;
}
// Register the states
protected registerStates(): void {
this.state.register(IntroScreen);
this.state.register(GameScreen);
}
}

Install the Dependencies

Once you have created the files, install the dependencies:

Terminal window
npm install

Run the Game

Terminal window
npm run dev