上一章讲的是键盘事件监听,实现马里奥的跳跃功能,本章讲述马里奥和大地(砖块)的碰撞检测,代码有点多,原视频长度差不多1个小时20分钟,大家可以看原视频进行理解,边看边敲,我在这里写的代码和注释仅我自己的理解,
本章的提交937989dbe9a60259cc55965db7eb4019cffba6c3、c19d8d5b85120b606eba0a149e4df0685a13dcfd
github地址:ainuo5213的超级马里奥
本节目录:
目录文件讲解:
1. traits/GoTrait.js:改变马里奥x方向的速度,当按下左键和右键的时候改变速度方向以此达到马里奥行走的效果
2. Math.js:原Velocity.js,改名为Math.js,存储一些关于数学相关的数据,例如矩阵(Matrix)、矢量(Vector)
3. Level.js:游戏的关卡对象,内部含有实体数据、碰撞检测、tile格子和更新方法等
4. setupKeyboard.js:抽离原本在入口文件中对于事件监听的代码到新的模块,并加入左右键的事件监听
5. TileCollider.js:碰撞检测类,用于检测某个实体和tile格子中大地格子(砖块)之间的碰撞
6. TileResolver.js:获取tile格子数据的辅助类,通过各种手段获取tile格子及其相关的数据
本节实现效果
入口文件变动
入口文件拆分了原本事件监听到单独的模块,加入了碰撞检测layer的挂载,然后添加了对于鼠标左键和鼠标移动的事件监听处理
玛丽奥创建模块变动
马里奥创建模块中,马里奥去除了速度的特征(因为要检测碰撞改变速度)、加入了移动的特征
加载模块变动
加载模块改动了加载关卡数据方法,在其中 加入了layer的创建和tile格子矩阵数据的初始化
图层模块变动
图层模块将原本绘制背景的方法去除,改动创建背景图层方法,将其中循环绘制背景的代码去掉,改为循环tile矩阵数据进行背景的绘制。
键盘状态模块改动
键盘状态模块将原本使用keyCode方式改为了code
实体模块改动
实体模块将原本Velocity对象改为了Victor对象
新增Go特征
import { Trait } from '../Entity.js'
export class GoTrait extends Trait {
constructor() {
super('go');
// 时长
this.dir = 0;
// 行走速度
this.speed = 6000;
}
// 更新马里奥的x方向的速度(含当前马里奥的方向)
update = (entity, deltaTime) => {
entity.vel.x = this.speed * this.dir * deltaTime;
}
}
新增Level对象
level对象包含整个游戏中所有数据,包括碰撞检测、实体列表、背景tile格子等数据
import { Compositor } from "./Compositor.js"
import TileCollider from "./TileCollider.js"
import { Matrix } from "./Math.js"
export class Level {
constructor() {
// 重力加速度
this.gravity = 2000;
// 组合器
this.compositor = new Compositor();
// 实体列表
this.entities = new Set();
// tile格子
this.tiles = new Matrix();
// tile格子碰撞检测类
this.tileCollider = new TileCollider(this.tiles);
}
update = deltaTime => {
// 循环每一个实体,调用其更新方法后,再更新其位置信息,进行碰撞检测(碰撞检测内部会更改实体的位置,如果实体真的发生了碰撞的话)
this.entities.forEach(entity => {
entity.update(deltaTime);
entity.pos.x += entity.vel.x * deltaTime;
this.tileCollider.checkX(entity);
entity.pos.y += entity.vel.y * deltaTime;
this.tileCollider.checkY(entity);
entity.vel.y += this.gravity * deltaTime;
});
}
}
新增Math对象
修改原本的Velocity.js为Math对象,在内部新增了Matrix对象,用于记录背景tile格子数据
// 矢量对象
export class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
set(x, y) {
this.x = x;
this.y = y;
}
}
// 矩阵数据,是个二维数组,存储渲背景(sky和ground)的每个单元格(16x16)的数据,数据例子:
// [
// [{ tile: 'groud'}, { tile: 'sky' }], []
// ]
export class Matrix {
constructor() {
this.grid = [];
}
forEach = callback => {
this.grid.forEach((column, x) => {
column.forEach((tile, y) => {
callback(x, y, tile);
})
});
}
get = (x, y) => {
const col = this.grid[x];
if (col !== undefined) {
return col[y];
}
return undefined;
}
set = (x, y, value) => {
if (this.grid[x] === undefined) {
this.grid[x] = [];
}
this.grid[x][y] = value;
}
}
键盘监听事件处理抽离
将原本位于入口文件中的事件监听移到了单独的模块,用来内聚事件监听
import { KeyboardState, CODE_SPACE, STATE_KEYDOWN, CODE_RIGHT, CODE_LEFT } from './KeyboardState.js'
function setupKeyboard(mario) {
// 监听键盘的空格事件
const keyboard = new KeyboardState();
// 添加键盘空格键的映射,并设置一个回调来使马里奥跳跃(设置y方向的速度)
keyboard.addMapping(CODE_SPACE, keyState => {
if (keyState === STATE_KEYDOWN) {
mario.jump.start();
}
else {
mario.jump.cancel();
}
});
// 左键和右键控制马里奥x方向速度的方向
keyboard.addMapping(CODE_RIGHT, keyState => {
mario.go.dir = keyState;
});
keyboard.addMapping(CODE_LEFT, keyState => {
mario.go.dir = -keyState;
});
return keyboard;
}
export default setupKeyboard;
马里奥碰撞检测类
马里奥碰撞检测类用于对马里奥移动过程中检测马里奥是否和砖块碰撞,如果碰撞了就改动马里奥的位置
import TileResolver from "./TileResolver.js"
export default class TileCollider {
constructor(tiles) {
this.tiles = tiles;
this.tileResolver = new TileResolver(tiles);
}
/**
* 马里奥Y方向的碰撞检测
* @param {Entity} entity 马里奥实体
*/
checkY = entity => {
// 这里不检测马里奥当前这个格子,做一个优化
let y;
if (entity.vel.y > 0) {
y = entity.pos.y + entity.size.y
} else if (entity.vel.y < 0) {
y = entity.pos.y
} else {
return;
}
// 找马里奥当前所在的格子范围,然后对每个格子进行碰撞检测:即马里奥y方向的高度和马里奥自己的高度与匹配出的格子高度对比
const matches = this.tileResolver.searchByRange(
entity.pos.x,
entity.pos.x + entity.size.x,
y,
y);
matches.forEach(match => {
if (!match) {
return;
}
// 跳过非大地的tile
if (match.tile.name !== "ground") {
return;
}
if (entity.vel.y > 0) {
if (entity.pos.y + entity.size.y > match.y1) {
entity.pos.y = match.y1 - entity.size.y;
entity.vel.y = 0;
}
} else if (entity.vel.y < 0) {
if (entity.pos.y < match.y2) {
entity.pos.y = match.y2;
entity.vel.y = 0;
}
}
});
}
checkX = entity => {
// 原理同checkY
let x;
if (entity.vel.x > 0) {
x = entity.pos.x + entity.size.x
} else if (entity.vel.x < 0) {
x = entity.pos.x
}
else {
return;
}
const matches = this.tileResolver.searchByRange(
x,
x,
entity.pos.y,
entity.pos.y + entity.size.y);
matches.forEach(match => {
if (!match) {
return false;
}
if (match.tile.name !== "ground") {
return false;
}
if (entity.vel.x > 0) {
if (entity.pos.x + entity.size.x > match.x1) {
entity.pos.x = match.x1 - entity.size.x;
entity.vel.x = 0;
}
} else if (entity.vel.x < 0) {
if (entity.pos.x < match.x2) {
entity.pos.x = match.x2;
entity.vel.x = 0;
}
}
});
}
}
格子矩阵数据获取辅助类
该类用于通过各种手段获取某个格子的矩阵数据和相关其他数据,例如始末位置等
export default class TileResolver {
constructor(matrix, tileSize = 16) {
this.matrix = matrix;
this.tileSize = tileSize;
}
// 获取当前x或y位置所在的格子索引
toIndex = pos => {
return Math.floor(pos / this.tileSize);
}
// 将x和x1或y和y1两个位置,转化为其之间的相隔的格子索引。例如toIndex(17, 33) => [1, 2]
toIndexRange = (pos1, pos2) => {
const posMax = Math.ceil(pos2 / this.tileSize) * this.tileSize;
const range = [];
let pos = pos1;
do {
range.push(this.toIndex(pos));
pos += this.tileSize;
} while (pos < posMax);
return range;
}
// 通过格子所在的索引获取tile数据,包括tile、当前格子所在位置始末(含x和y方向)
getByIndex = (indexX, indexY) => {
const tile = this.matrix.get(indexX, indexY);
if (tile) {
const y1 = indexY * this.tileSize;
const y2 = y1 + this.tileSize;
const x1 = indexX * this.tileSize;
const x2 = x1 + this.tileSize;
return {
tile,
x1,
x2,
y1,
y2,
}
}
}
// 通过位置找tile数据,现将其转换为格子所在的索引,再找出格子的tile和当前格子的位置始末数据
searchByPosition = (positionX, positionY) => {
const indexX = this.toIndex(positionX);
const indexY = this.toIndex(positionY);
return this.getByIndex(indexX, indexY);
}
// 通过传入格子所在的始末位置数据找格子所在tile数据范围,并形成格子始末位置所对应的始末tile数据范围数组
searchByRange = (x1, x2, y1, y2) => {
const mathes = [];
this.toIndexRange(x1, x2)
.forEach(indexX => {
this.toIndexRange(y1, y2)
.forEach(indexY => {
const match = this.getByIndex(indexX, indexY);
if (match) {
mathes.push(match);
}
});
});
return mathes;
}
}
今天代码有点多,各位可以看原视频更好理解,代码仅作参考