# Platformer camera and parallax

> A camera that follows the hero the way the consoles did, with a dead zone, the edges of the map and a shake, and clouds and hills behind the level that move slower than it.

Example `camera-follow-parallax`, running at https://nacatamalon.com/en/examples/camera-follow-parallax. The whole catalogue: https://nacatamalon.com/llms/examples.md.

`useCameraFollow` keeps the hero in view: a dead zone in the middle (the white frame) that the hero walks inside without the camera moving, never past the edges of the map (`bounds: map`), on whole pixels. `shake` makes it tremble, and dies away on its own.

Behind the level, `createBackdrop` lays clouds and hills endlessly across the world, each moving with the camera only so much (`parallax`): a tenth for the clouds, four tenths for the hills. The brick wall is a single frame of the tile sheet repeated across one sprite (`tile: true`).

← → walk, Z jump, X shake.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { Level } from './scenes/level';
import { ACTIONS, WIDTH, HEIGHT } from './settings';

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#9fd4ff',
    step: 60,
    actions: ACTIONS
});

// Declare Scenes
game({
    Level
});
```

`components/hero.ts`:

```ts
import { createSprite, useActions, useLoadTexture, useTileBody2d, useUpdate } from 'nacatamalon';
import { HERO, START } from '../settings';

export const Hero = () => {
    const sprite = createSprite({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/hero.png'
        }),
        transform: {
            x: START.x,
            y: START.y
        }
    });

    const body = useTileBody2d({
        width: 12,
        height: 22,
        offset: [0, 1]
    });

    const input = useActions();

    useUpdate(() => {
        const walk = input.value('right') - input.value('left');

        body.velocity.x = walk * HERO.speed;

        if (walk !== 0) {
            sprite.flipX = walk < 0;
        }

        if ((input.justPressed('jump') || input.justPressed('up')) && body.isOnGround) {
            body.velocity.y = -HERO.jumpSpeed;
        }

        if (input.justPressed('down') && body.isOnOneWay) {
            body.dropThrough();
        }

        // Falling into a pit changes the body's position, so collision and drawing return together.
        if (sprite.transform.y > HERO.respawnY) {
            body.setPosition(START.x, START.y);
            body.velocity.y = 0;
        }
    });
};
```

`components/hud.ts`:

```ts
import { createSprite, createText, useScreenSpace } from 'nacatamalon';
import { DEADZONE, WIDTH, HEIGHT, ZOOM } from '../settings';

export const Hud = () => {
    useScreenSpace();

    // Screen-space drawing ignores the camera's zoom, so the outline is scaled explicitly.
    const width = DEADZONE.width * ZOOM;
    const height = DEADZONE.height * ZOOM;
    const left = (WIDTH - width) / 2;
    const top = (HEIGHT - height) / 2;

    const edges = [[left, top, width, ZOOM], [left, top + height, width, ZOOM], [left, top, ZOOM, height], [left + width, top, ZOOM, height]];

    for (const [x, y, w, h] of edges) {
        createSprite({
            width: w,
            height: h,
            tint: '#ffffff55',
            anchor: {
                x: 0,
                y: 0
            },
            transform: {
                x,
                y
            }
        });
    }

    createText({
        text: 'ARROWS / WASD WALK  Z / SPACE JUMP  X SHAKE',
        transform: {
            x: 8,
            y: 8
        },
        tint: '#40507a'
    });
};
```

`components/scenery.ts`:

```ts
import { createBackdrop, createSprite, createSpriteAtlas, useLoadTexture } from 'nacatamalon';

export const Scenery = () => {
    // Far to near. Each image repeats across the world and follows a share of the camera's move.
    createBackdrop({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/clouds.png'
        }),
        parallax: {
            x: 0.1,
            y: 0.1
        },
        y: 30
    });
    createBackdrop({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/hills.png'
        }),
        parallax: {
            x: 0.4,
            y: 0.4
        },
        y: 150
    });

    const tiles = createSpriteAtlas({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/tiles.png'
        }),
        columns: 8,
        rows: 3
    });

    createSprite({
        atlas: tiles,
        frame: 2,
        width: 96,
        height: 48,
        tile: true,
        anchor: {
            x: 0,
            y: 1
        },
        transform: {
            x: 112,
            y: 272
        }
    });
};
```

`scenes/level.ts`:

```ts
import { createScene, createTilemap, useAction, useCamera2d, useCameraFollow, useSpawn, useUpdate } from 'nacatamalon';
import { Hero } from '../components/hero';
import { Hud } from '../components/hud';
import { Scenery } from '../components/scenery';
import { DEADZONE, SHAKE, ZOOM } from '../settings';

export const Level = () => {
    useCamera2d({
        zoom: ZOOM
    });
    useSpawn(Scenery)();

    const map = createTilemap({
        src: '/example-assets/assets/platformer/wide.tilemap'
    });

    const hero = useSpawn(Hero)();

    const follow = useCameraFollow(hero, {
        deadzone: DEADZONE,
        bounds: map
    });

    const shake = useAction('action');

    useUpdate(() => {
        if (shake.justPressed()) {
            follow.shake(SHAKE.strength, SHAKE.seconds);
        }
    });

    useSpawn(Hud)();

    return createScene();
};
```

`settings.ts`:

```ts
import { INPUT } from 'nacatamalon';

export const ZOOM = 2;
export const WIDTH = 216 * ZOOM;
export const HEIGHT = 160 * ZOOM;
export const ACTIONS = INPUT.ACTION_PRESETS.platformer;

export const DEADZONE = {
    width: 48,
    height: 64
};

export const START = {
    x: 40,
    y: 250
};

export const HERO = {
    speed: 110,
    jumpSpeed: 330,
    respawnY: 400
};

export const SHAKE = {
    strength: 4,
    seconds: 0.4
};
```
