这段时间心血来潮,在网上找了一大堆前端关于游戏制作的博客,诸如飞机大战、魂斗罗等,但是个人思维限制,可能想的不是很多、很全面,最终放弃了个人独立开发的打算。皇天不负有心人,最终在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主写的代码很好,思路很清晰,我自己第一次看到也觉得需要好好的理解一番,当然各位小伙伴可以去看看视频,雀食不错。
|