原生JS超级马里奥(第六天)

本章节介绍了如何在超级马里奥游戏中配置第一关关卡数据,包括sprites/overworld.json和sprites/underworld.json中的tile格子数据。同时,详细讲解了Camera.js中的地图相机对象,以及debug.js中的鼠标控制功能,实现了地图的动态渲染和鼠标右键拖动时的地图移动。此外,还展示了如何通过修改组合器Compositor和layer实现动态渲染和碰撞检测。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  上一章讲述马里奥和大地(砖块)的碰撞检,本章讲解第一关关卡数据生成和其他砖块的生成,原视频50分钟,本章比上一章简单一点,基本都是修改原有配置和加入新的配置

本章的提交4a3203c762cee3402c45fb6aef393f4ede0790a7

github地址:ainuo5213的超级马里奥

本节目录

 目录讲解:

        1. sprites/overworld.json:第一关所需要用到的tile格子数据配置,包括tile格子切片位置(相对于原始图片)、关卡所需要的tile格子种类和大小等等

        2. sprite/underworld.json:和sprites/overworld.json类似,都是关卡所需要的图片切片资源来源

        3. Camera.js:马里奥移动过程中范围的相机,他的范围比游戏范围略小,可以用于调试的时候用

        4. debug.js:鼠标左键和右键移动过程中对于马里奥位置的摆放、地图的移动等,用于调试

本节实现效果

 入口文件改动

入口文件移除了对于鼠标左键和右键的监听,将其单独移到了debug.js,并且加入了新的layer——相机layer

 组合器Compositor变动

组合器加入了地图相机对象,用于执行每一个layer方法传递参数

 layer变动

layer变动较大,主要是加入了地图相机对象,使得每个方法都有部分变动,改动最大的是绘制背景图层方法,加入了动态渲染地图数据,所以在渲染时需要计算当前相机的位置和长度来得到当前走过的tile格子索引,根据tile格子索引来渲染背景

/**
 * 创建绘制图像的函数:在回调中绘制图像
 * @param {Level} level Level对象,由loader导出的
 * @param {UnitStyleSheet} backgroundSprite 单元图像样式对象
 * @returns {Function} 绘制图像的回调
 */
export function createBackgroundLayer(level, backgroundSprite) {
    const tiles = level.tiles;
    const tileResolver = level.tileCollider.tileResolver;

    // 创建临时缓存区,使用闭包在外部绘制图像
    const buffer = document.createElement("canvas");
    buffer.width = 256 + 16; // 减少渲染长度,使该方法支持地图的动态渲染
    buffer.height = 240;

    const bufferContext = buffer.getContext("2d");
    let startIndex, endIndex;


    // 动态渲染地图数据
    function redraw(drawFrom, drawTo) {

        if (drawFrom === startIndex && drawTo === endIndex) {
            return;
        }

        startIndex = drawFrom;
        endIndex = drawTo;

        for (let x = startIndex; x <= endIndex; x++) {
            const col = tiles.grid[x];
            if (col) {
                col.forEach((tile, y) => {
                    backgroundSprite.drawTile(tile.name, bufferContext, x - drawFrom, y);
                })
            }
        }
    }

    // 返回绘制图像的回调
    return function drawBackgroundLayer(context, camera) {

        // 动态渲染地图所需要的数据参数
        const drawWidth = tileResolver.toIndex(camera.size.x);
        const drawFrom = tileResolver.toIndex(camera.pos.x);
        const drawTo = drawWidth + drawFrom;
        redraw(drawFrom, drawTo);

        // 这里%16是因为当camera移动的过程中,camera的x值会越来越大,绘制的时候会出现问题,所以%16时,无论camera怎么移动最终的值也不会超过16
        context.drawImage(buffer, -camera.pos.x % 16, -camera.pos.y);
    }
}

/**
 * 创建绘制图像的函数:在回调中绘制图像
 * @param {Entity} entities 多个实体
 * @param {number} width 实体宽
 * @param {number} height 实体高
 * @returns 绘制图像的回调
 */
export function createrSpriteLayer(entities, width = 64, height = 64) {
    const spriteBuffer = document.createElement("canvas");

    spriteBuffer.width = width;
    spriteBuffer.height = height;
    const spriteBufferContext = spriteBuffer.getContext("2d");


    return function drawSpriteLayer(context, camera) {
        entities.forEach(entity => {
            spriteBufferContext.clearRect(0, 0, width, height);
            entity.draw(spriteBufferContext);
            context.drawImage(
                spriteBuffer,
                entity.pos.x - camera.pos.x,
                entity.pos.y - camera.pos.y);
        });
    }
}

/**
 * 创建碰撞检测的函数
 * @param {Level} level 关卡实例对象
 * @returns 
 */
export function createCollisionLayer(level) {
    const resolveTiles = [];
    const tileResolver = level.tileCollider.tiles;
    const tileSize = tileResolver.tileSize;

    // 重写碰撞检测,将每一个匹配到的x和y加入临时数组(我认为在这里仅作为调试用,我认为甚至不写都行)
    const getByIndexOriginal = tileResolver.getByIndex;
    tileResolver.getByIndex = function getByIndexFake(x, y) {
        resolveTiles.push({ x, y });
        return getByIndexOriginal.call(tileResolver, x, y);
    }

    return function drawCollisionLayer(context, camera) {
        context.strokeStyle = "blue";
        resolveTiles.forEach(({ x, y }) => {
            context.beginPath();
            context.rect(
                x * tileSize - camera.pos.x,
                y * tileSize - camera.pos.y,
                tileSize,
                tileSize);
            context.stroke();
        });

        context.strokeStyle = "red";
        level.entities.forEach(entity => {
            context.beginPath();
            context.rect(
                entity.pos.x - camera.pos.x,
                entity.pos.y - camera.pos.y,
                entity.size.x,
                entity.size.y);
            context.stroke();
        });

        resolveTiles.length = 0;
    }
}

// 创建地图相机对象的UI,用于调试
export function createCameraLayer(cameraToDraw) {
    return function drawCameraLayer(context, fromCamera) {
        context.strokeStyle = "purple";
        context.beginPath();
        context.rect(
            cameraToDraw.pos.x - fromCamera.pos.x,
            cameraToDraw.pos.y - fromCamera.pos.y,
            cameraToDraw.size.x,
            cameraToDraw.size.y);
        context.stroke();
    }
}

加载器loader变动

加载器loader改动较多

        1. loadTiles中加入了当前tile格子的类型type,后续碰撞检测使用type进行格子类型判断

        2. loadTiles的配置逻辑修改,原配置文件中range范围表示的含义为[xStart, xEnd, yStart, yEnd],现改为[xStart, xLen, yStart, yLen],并根据range长度不同,每一位代表的意义也不同

        3. loadLevelAsync方法逻辑变动,将关卡数据和关卡所需要的tile格子背景切片所需要的数据合为一起加载

import { createBackgroundLayer, createrSpriteLayer } from "./layers.js";
import { Level } from "./Level.js";
import UnitStyleSheet from "./UnitStyleSheet.js";

export function loadImageAsync(url) {
    return new Promise(resolve => {
        const img = new Image();
        img.onload = function () {
            resolve(img);
        }
        img.src = url;
    });
}

function loadTiles(level, backgrounds) {
    function applyRange(background, xStart, xLen, yStart, yLen) {
        const xEnd = xStart + xLen;
        const yEnd = yStart + yLen;
        for (let x = xStart; x < xEnd; x++) {
            for (let y = yStart; y < yEnd; y++) {
                level.tiles.set(x, y, {
                    name: background.tile,
                    type: background.type,
                });
            }
        }
    }
    // 将地图数据中的每一个背景图层都加载到level对象的tiles中,为以后进行碰撞检测打下数据基础
    backgrounds.forEach(background => {
        background.ranges.forEach(range => {

            // 修改渲染逻辑: 当配置中的range为4位数,则其分别为x位置开始xStart、x方向渲染长度xLen、y位置开始yStart、y方向渲染长度yLen
            //             当配置中的range为2位数,则其分别为x位置开始xStart、y位置开始yStart,此时yLen、xLen均为1
            //             当配置中的range为3位数,则其分别为x位置开始xStart、x方向渲染长度xLen、y位置开始yStart,此时yLen为1
            if (range.length === 4) {
                const [xStart, xLen, yStart, yLen] = range;
                applyRange(background, xStart, xLen, yStart, yLen);
            } else if (range.length === 2) {
                const [xStart, yStart] = range;
                applyRange(background, xStart, 1, yStart, 1);
            } else if (range.length === 3) {
                const [xStart, xLen, yStart] = range;
                applyRange(background, xStart, xLen, yStart, 1);
            }
        })
    })
}

function loadSpriteSheet(name) {
    return loadJson(`/src/sprites/${name}.json`)
        .then(data => Promise.all([data, loadImageAsync(data.tileImgUrl)]))
        .then(([data, image]) => {
            const backgroundUnitStyleSheet = new UnitStyleSheet(image, data.tileWidth, data.tileHeight);
            data.tiles.forEach(tile => {
                backgroundUnitStyleSheet.defineTile(tile.name, tile.index[0], tile.index[1]);
            })
            return backgroundUnitStyleSheet;
        });
}

function loadJson(url) {
    return fetch(url).then(r => r.json());
}

export function loadLevelAsync(name) {
    return loadJson(`/src/levels/${name}.json`)
        .then(data => Promise.all([data, loadSpriteSheet(data.spriteSheet)]))
        .then(([levelJson, backgroundSprite]) => {
            const level = new Level();

            // 加载level中的matrix每一个格子的数据到tiles
            loadTiles(level, levelJson.backgrounds);

            // 创建绘制背景的回调
            const backgroundLayer = createBackgroundLayer(level, backgroundSprite);
            level.compositor.layers.push(backgroundLayer);

            // 创建马里奥图像的回调
            const marioSpriteLayer = createrSpriteLayer(level.entities);
            level.compositor.layers.push(marioSpriteLayer);

            return level;
        })
}

创建马里奥方法变动

创建马里奥时,在绘制的时候,使用0,0点替代原本的this.pos.x等

 加载静态资源Sprite文件变动

去除了原本的`loadBackgroundSprites`方法,将其合并到了加载器中 

碰撞检测文件TileCollider变动

碰撞检测中使用tile格子数据的type替代tile各自数据的name,来判断格子类型

地图相机对象Camera.js

import { Vector } from "./Math.js"


export default class Camera {
    constructor() {
        this.pos = new Vector(0, 0);
        this.size = new Vector(256, 224);
    }
}

调试文件debug.js

调试文件即原本位于main方法中的鼠标监听,将其移动到了debug.js

export function setupMouseControl(canvas, entity, camera) {
    let lastEvent;
    ["mousedown", "mousemove"].forEach(eventName => {
        canvas.addEventListener(eventName, event => {
            // 如果按了鼠标左键,就将马里奥拖拽(设置速度为0,位置为点击时和鼠标移动时的位置)
            if (event.buttons === 1) {
                entity.vel.set(0, 0);
                entity.pos.set(
                    event.offsetX + camera.pos.x,
                    event.offsetY + camera.pos.y);
            } else if (
                event.buttons === 2 &&
                lastEvent &&
                lastEvent.buttons === 2 &&
                lastEvent.type === "mousemove") {
                camera.pos.x -= event.offsetX - lastEvent.offsetX;
            }
            lastEvent = event;
        });
    });
    canvas.addEventListener("contextmenu", e => {
        e.preventDefault();
    })
}

本章主要是实现关卡数据的配置和渲染和鼠标右键拖动时,地图的移动等。代码不多,可以看着原视频敲一遍,代码仅供参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值