上一章讲述马里奥和大地(砖块)的碰撞检,本章讲解第一关关卡数据生成和其他砖块的生成,原视频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();
})
}
本章主要是实现关卡数据的配置和渲染和鼠标右键拖动时,地图的移动等。代码不多,可以看着原视频敲一遍,代码仅供参考。