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

这段时间心血来潮,在网上找了一大堆前端关于游戏制作的博客,诸如飞机大战、魂斗罗等,但是个人思维限制,可能想的不是很多、很全面,最终放弃了个人独立开发的打算。皇天不负有心人,最终在youtube上找到了前端关于超级马里奥的视频,链接见Super Mario In Javascript

打算两周时间,每天花点时间看一到两集,并将所看视频整理出思路和代码供大家参考,当然也会讲解一些自己探寻出来的原理,并写明注释

github链接见:ainuo5213的超级马里奥,可以从该链接获取源码和素材,我争取做到每天更新直至结束,小伙伴们也可以自己再此基础上发挥或重构

第一张实现效果图如下:

目录结构

 目录文件讲解:

1-1.json:目前存储关卡的数据,比如背景渲染的一些东西

loader.js:导入配置相关比如图片资源、关卡数据资源等

main.js:入口文件

UnitStyleSheet.js:渲染背景的类文件

Canvas裁剪图片

在此之前我需要先讲解一下canvas裁剪图片的原理,canvas.drawImage,通常传递5个数据,分别是image/video/canvas、x、y、width、height,但是另外还可以传递4个参数,用于裁剪图片,详情见HTML5 canvas drawImage() 方法,举个例子:

const canvas = document.getElementById("screen");
const context = canvas.getContext("2d");

loadImageAsync("/src/assets/tiles.png")
    .then(image => {
        canvas.getContext("2d")
            .drawImage(
                image,
                0,
                0,
                16,
                16,
                0,
                0,
                16,
                16);
    });

上述方法描述为:从(0, 0)裁剪一个16x16的图像并放置到(0,0)且16x16的一个方块内,图像显示为

 原图:

 可以看到我们能够成功将第一个方块剪切出来。

导入配置相关:

// 异步导入图片
export function loadImageAsync(url) {
    return new Promise(resolve => {
        const img = new Image();
        img.onload = function () {
            resolve(img);
        }
        img.src = url;
    });
}

// 异步导入关卡数据
export function loadLevelAsync(name) {
    return fetch(`/src/levels/${name}.json`)
        .then(r => r.json());
}

单位图像样式对象

export default class UnitStyleSheet {

    /**
     * 创建一个图像样式定义对象
     * @param {HTMLImageElement} image 背景图像
     * @param {number} width 单位图像宽度
     * @param {number} height 单位图像高度
     */
    constructor(image, width, height) {
        this.image = image;
        this.width = width;
        this.height = height;
        this.tiles = new Map();
    }

    /**
     * 存储需要裁剪的图片
     * @param {string} name 画布名称
     * @param {number} x 需要裁剪的图片x与单位高度倍数
     * @param {number} y 需要裁剪的图片y与单位高度倍数
     */
    define(name, x, y) {
        const canvas = document.createElement("canvas");
        canvas.width = this.width;
        canvas.height = this.height;

        // 裁剪图片,并存储
        canvas.getContext("2d")
            .drawImage(
                this.image,
                x * this.width,
                y * this.height,
                this.width,
                this.height,
                0,
                0,
                this.width,
                this.height);
        this.tiles.set(name, canvas);
    }

    /**
     * 画图像
     * @param {string} name 画布名称
     * @param {any} context 上下文对象
     * @param {number} x 需要画图像的x坐标
     * @param {number} y 需要画图像的y坐标
     */
    draw(name, context, x, y) {
        const canvas = this.tiles.get(name);
        context.drawImage(canvas, x, y, this.width, this.height); // drawImage第一个参数接收类型:图像、视频、画布
    }

    /**
     * 画单元格
     * @param {string} name 画布名称
     * @param {any} context 上下文对象
     * @param {number} x 需要画图像的x坐标(倍数)
     * @param {number} y 需要画图像的y坐标(倍数)
     */
    drawTile(name, context, x, y) {
        this.draw(name, context, x * this.width, y * this.height)
    }
}

入口函数:

import UnitStyleSheet from "./UnitStyleSheet.js";
import { loadImageAsync, loadLevelAsync } from "./loader.js";

// 用于绘制背景,双重循环的x和y依赖于关卡配置文件
function drawBackground(background, context, unitStyleSheet) {
    background.ranges.forEach(([x1, x2, y1, y2]) => {
        for (let x = x1; x < x2; x++) {
            for (let y = y1; y < y2; y++) {
                unitStyleSheet.drawTile(background.tile, context, x, y);
            }
        }
    })
}

const canvas = document.getElementById("screen");
const context = canvas.getContext("2d");

loadImageAsync("/src/assets/tiles.png")
    .then(image => {
        // 创建单位图像样式对象,设置单位长度,并定义裁剪起始位置来裁剪图像
        const unitStyleSheet = new UnitStyleSheet(image, 16, 16);
        unitStyleSheet.define("ground", 0, 0);
        unitStyleSheet.define("sky", 3, 23);
        loadLevelAsync('1-1')
            .then(level => {
                level.backgrounds.forEach(background => {
                    drawBackground(background, context, unitStyleSheet);
                })
            });
    });

当然油管UP主写的代码很好,思路很清晰,我自己第一次看到也觉得需要好好的理解一番,当然各位小伙伴可以去看看视频,雀食不错。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>