[BS-00] Barebones

posted

2023-05-04

updated

2023-05-07
[BS-00] Barebones - Teaser

The starting point for Baby Steps


In this post i talked about the Baby-Steps Series i am making.
This project is the very first part and marks the base for all projects to come.

Readers Notice: I am expecting you to know basic typescript and how to setup a npm project itself.

Features


This project provides the build-setup and an empty canvas that gets scaled to the screen.

Details - Setup


The folder structure looks as follows 
/releases        ; target for the released bundles
/dist            ; target for the compiled files
/src             ; the projects sourcecode 
/src/index.html  ; main entrypoint of the project
/src/index.ts    ; entrypoint for the typescript
/src/style.css   ; entrypoint for the styling
/src/library     ; generalized code, that i plan to reuse in upcomming projects
/cover.png       ; every project gets a cover image
/README.md       ; short project description
/package.json    ; project description for npm
/tsconfig.json   ; typescript compile settings
/vite.config.js  ; configuration for the vite-bundler

The entire project is build with typescript and i use Vite to bundle the project into a dist folder after that the folder get packed into a zip file to be able to upload it easily.
So these are my only dependencies
  "devDependencies": {
    "typescript": "^4.0.3",
    "vite": "^4.3.4",    
    "vite-plugin-zip-pack": "^1.0.5"
  }

And the scripts look like this 
  "scripts": {
    "dev": "vite serve",
    "test": "vite serve",
    "preview": "vite preview",
    "build": "vite build"
  }

The "vite.config.js" is configured like follows.
import { defineConfig } from "vite";
import zipPack from "vite-plugin-zip-pack";

export default defineConfig({
    root: "./src",
    base: "./",
    publicDir: false,
    server: {
        outDir: "../dist",
    },
    build: {
        outDir: "../dist",
        emptyOutDir: true,
    },
    plugins: [zipPack({
        inDir: "./dist",
        outDir: "./releases",
        outFileName: "release-" + process.env.npm_package_name + "-" + process.env.npm_package_version + ".zip",
    })],
});



Details - Html


The html is also very simplistic.
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>00 Barebones</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body tabindex="2">
    <div id="app" tabIndex="1" >
        <canvas id="canvas" width="800" height="600"></canvas>
    </div>
    <script src="./index.ts" type="module"></script>
</body>

</html>
I have basically only linked the different entry points for style and code 
and then created an "app" container with a canvas inside that we will interact with.

Besides that i have given the canvas and the body element the "tabindex" attribute.
This enables the browser to switch the focus between them, so that either the app or the body (background) is selected.
This way i can later catch all events while inside the "app" whilst still allowing normal browser interaction when clicking outside the canvas (to open the dev-console for example).

Details - Styling


The styling is also just the minimum 
* {
    box-sizing: border-box;
}

body {
    --border: 2rem;
    display: flex;
    width: 100%;
    height: 100vh;
    padding: var(--border);
    margin: 0;

    background: lightgray;

    justify-content: center;
    align-items: start;
}

#app {
    display: flex;
    max-width: calc(100vw - var(--border) * 2);
    max-height: calc(100vh - var(--border) * 2);
    width: min(100%, (100vh - var(--border) * 2)* 4 / 3);
    height: auto;
    justify-content: center;
    align-items: center;
    background-color: #111;

    outline: black 1px solid;
}

#app:focus {
    outline: red 1px solid;
}

canvas {
    display: flex;
    width: 100%;
    height: 100%;
}

It all is build to center the canvas in place.
Also if the canvas is focused it will have a red outline to indicate that.

Details - Code


The high level code convention for these project should be MVC (Model, View, Controller).
The rough idea behind it is, that the project logic should be encoded only in the Model.
And vizualization happens only in the code for the View.
The Controller finally orchestrates the open ends between those two.
All of this will be wrapped in a Game class.

The Game will be initialized like this then
import { Game } from "./game/base/Game";
import * as tgt from "./library/index";

(async () => {
    const app = tgt.getElementById('app', HTMLDivElement);
    tgt.preventDefault(app);
    let game: Game = new Game(app);
    await game.run();
})();

We look for our App container, pass it to the Game class and wait for the game to finish.

We keep a reference to the Controller, View and Model.
export class Game {

    public controller: GameController;
    public view: GameView;
    public model: GameModel;

On construction we look for out canvas inside the container and set the initial state.
    public constructor(app: HTMLElement) {
        const canvas = tgt.getElementByQuerySelector(app, "canvas", HTMLCanvasElement);
        const context = canvas.getContext("2d");
        tgt.assertNotNull(context, "No 2d context found");
        this.view = new GameView(context);
        this.model = new GameModel();
        this.controller = new GameController(this.model);
    }

The run method is a Promise that will trigger the controllers "newGame" Method and kick off the update loop.
The resolve-mthod will be stored to call it later once the game has finished.
    public async run() {
        return new Promise<void>((resolve, reject) => {
            this.on_game_finished = resolve;
            this.controller.newGame();
            requestAnimationFrame(this.onFrame);
        });
    }

The update loop simply always repeats it self on the next animation frame
after updating and rendering with the controller and view.
    protected update(delta_ms: number) {
        this.controller.update(delta_ms);
        this.view.update(delta_ms);
        this.view.render(this.model);
    }

    protected onFrame = (timestamp_ms: number) =>  {
        this.update(timestamp_ms - this.last_time_ms);
        this.last_time_ms = timestamp_ms;
        if (this.controller.isGameOver()) {
            if(this.on_game_finished) this.on_game_finished();
        } else {
            requestAnimationFrame(this.onFrame);
        }
    }


Since there is no real game logic now the Model and Controller really only implement their methods with an empty body, doing nothing.
Only the view displays the "Example" text in the center.
export class GameView implements View {

    public constructor(
        public context: CanvasRenderingContext2D,
    ) {

    }

    public update(delta_ms: number) : void{
        // do nothing
    }

    public render(model: GameModel): void {
        this.reset_canvas_state();
        this.context.fillText("Example", 400, 300);
    }

    /**
     * Reset default canvas state and paint the background
     */
    protected resetCanvasState() {
        this.context.fillStyle = "#000";
        this.context.fillRect(0, 0, 800, 600);
        this.context.fillStyle = "#fff";
        this.context.font = "16px monospace";
        this.context.textAlign = "center";
        this.context.textBaseline = "middle";
        this.context.imageSmoothingEnabled = false;
    }
}


Open in Github


Releases

Comments

captcha
  • Test Comment from myself

    tobidot 2024-02-02

    Just testing the comment feature