上一章讲述部分代码重构,本章讲koopa和goomba的生成,和他们的部分动作
本章的提交ID:97bf843711642e39f52649399f2ca0138de0c104
github地址:ainuo5213的超级马里奥
本节目录
目录讲解:
删除了mario.js、VelocityTrait.js
1. entities/Goomba.js:蘑菇怪的实体,继承自Entity
2. entities/Koopa.js:乌龟的实体,继承自Entity
3. entities/Mario.js:原mario.js,继承自Entity
4. WalkTrait.js:用于行走的特征,继承自Trait
5. BoundingBox.js:用于计算每一个实体的边界,例如top、bottom、left、center等
6. entities.js:批量加载各种实体,避免在入口文件中一一加载
7. sprites/goomba.json:用于定义goomba的切片和动画帧
8. sprites/koopa.json:用于定义koopa的切片和动画帧
实现效果
入口文件改动
入口文件移除了createMario方法,导入了loadEntities方法,用于批量加载已生成的实体 ,并添加到level.entities
Entity.js改动
新增定义了Sides的方向,用于x方向的碰撞检测边
Entity抽象类新增offset和bounds,前者用于在碰撞检测时考虑偏移量,后者用于当前实体的边界对象取值,例如top、left、bottom、right、center等
layer.js改动
layer对于绘制碰撞检测的layer时,使用bounds对象的值,考虑偏移量
TileCollider.js改动
TileCollider改动由于本人将注释删掉,写法重构了一次,这里以代码进行展示,逻辑改动不多。
在计算碰撞检测的时候,替换原本x、y等,加入bounds,考虑偏移量的问题
import TileResolver from "./TileResolver.js"
import { Sides } from "./Entity.js"
export default class TileCollider {
constructor(tileMatrix) {
this.tiles = new TileResolver(tileMatrix);
}
checkX(entity) {
let x;
if (entity.vel.x > 0) {
x = entity.bounds.right;
} else if (entity.vel.x < 0) {
x = entity.bounds.left;
} else {
return;
}
const matches = this.tiles.searchByRange(
x, x,
entity.bounds.top, entity.bounds.bottom);
matches.forEach(match => {
if (match.tile.type !== 'ground') {
return;
}
if (entity.vel.x > 0) {
if (entity.bounds.right > match.x1) {
entity.bounds.right = match.x1;
entity.vel.x = 0;
entity.obstruct(Sides.RIGHT);
}
} else if (entity.vel.x < 0) {
if (entity.bounds.left < match.x2) {
entity.bounds.left = match.x2;
entity.vel.x = 0;
entity.obstruct(Sides.LEFT);
}
}
});
}
checkY(entity) {
let y;
if (entity.vel.y > 0) {
y = entity.bounds.bottom;
} else if (entity.vel.y < 0) {
y = entity.bounds.top;
} else {
return;
}
const matches = this.tiles.searchByRange(
entity.bounds.left, entity.bounds.right,
y, y);
matches.forEach(match => {
if (match.tile.type !== 'ground') {
return;
}
if (entity.vel.y > 0) {
if (entity.bounds.bottom > match.y1) {
entity.bounds.bottom = match.y1;
entity.vel.y = 0;
entity.obstruct(Sides.BOTTOM);
}
} else if (entity.vel.y < 0) {
if (entity.bounds.top < match.y2) {
entity.bounds.top = match.y2;
entity.vel.y = 0;
entity.obstruct(Sides.TOP);
}
}
});
}
}
sprites/mario.json改动
加入了马里奥的动画,用于创建马里奥的时候,去除在代码里面定义的动画和distance等(hard code)
新增entities/Goomba.js
用于定义Goomba对象 (蘑菇小怪)
import { Entity } from "../Entity.js"
import { WalkTrait } from "../traits/WalkTrait.js"
import { loadSpriteSheet } from "../loader.js";
export function loadGoomba() {
return loadSpriteSheet("goomba")
.then(createGoombaFactory)
}
function createGoombaFactory(goombaSprite) {
// 从配置里获取帧
const walkAnime = goombaSprite.animations.get("walk");
function drawGoomba(context) {
goombaSprite.draw(walkAnime(this.lifetime), context, 0, 0);
}
return function createGoomba() {
const goomba = new Entity();
goomba.size.set(16, 16);
goomba.vel.x = -30;
// 添加行走的特征
goomba.addTrait(new WalkTrait());
goomba.draw = drawGoomba;
return goomba;
}
}
新增entities/Koopa.js
用于定义Koopa对象(小乌龟怪)
import { Entity, Sides } from "../Entity.js"
import { WalkTrait } from "../traits/WalkTrait.js"
import { loadSpriteSheet } from "../loader.js";
export const LOW_SPEED = 1 / 1500;
export const HIGH_SPEED = 1 / 6000;
export function loadKoopa() {
return loadSpriteSheet("koopa")
.then(createKoopaFactory)
}
function createKoopaFactory(koopaSprite) {
// 从配置里获取动画
const walkAnime = koopaSprite.animations.get("walk");
function drawKoopa(context) {
koopaSprite.draw(walkAnime(this.lifetime), context, 0, 0, this.vel.x < 0);
}
return function createKoopa() {
const koopa = new Entity();
koopa.size.set(16, 16);
koopa.vel.x = -30;
// 设置y方向向下偏移8px
koopa.offset.y = 8;
// 添加walk特征
koopa.addTrait(new WalkTrait());
koopa.draw = drawKoopa;
return koopa;
}
}
新增entities/Mario.js
原mario.js,迁移到了该文件,并对其中做出了部分重构(闭包)
import { Entity } from "../Entity.js"
import { JumpTrait } from '../traits/JumpTrait.js'
import { GoTrait } from '../traits/GoTrait.js'
import { loadSpriteSheet } from "../loader.js";
import { createAnim } from "../animation.js";
export const LOW_SPEED = 1 / 1500;
export const HIGH_SPEED = 1 / 7000;
export function loadMario() {
return loadSpriteSheet("mario")
.then(createMarioFactory)
}
function createMarioFactory(marioSprite) {
const getFrame = marioSprite.animations.get("run");
// 马里奥运动时取得其动画帧名字进行对应帧动画对应的切片,进行渲染
function routeFrame(mario) {
if (mario.jump.isFalling) {
return "mario-jump";
}
// break的帧需要马里奥当前速度方向和当前运动方向相反即可
if (mario.vel.x * mario.go.dir < 0) {
return "mario-break"
}
if (mario.go.distance > 0) {
return getFrame(mario.go.distance);
}
return "mario";
}
function drawMario(context) {
marioSprite.draw(routeFrame(this), context, 0, 0, this.go.heading < 0);
}
function setTurboState(turboOn) {
this.go.dragFactor = turboOn ? HIGH_SPEED : LOW_SPEED;
}
return function createMario() {
const mario = new Entity();
mario.size.set(14, 16);
mario.draw = drawMario;
// 添加马里奥的特征:1、跳跃;2、方向(第五节删除了速度,因为碰撞检测的时候才会对速度特征进行修改,而非update的时候进行修改)
mario.addTrait(new GoTrait());
mario.addTrait(new JumpTrait());
mario.turbo = setTurboState;
mario.turbo(false);
return mario;
}
}
新增trait/WalkTrait.js
用于定义行走的特征
import { Sides, Trait } from '../Entity.js'
export class WalkTrait extends Trait {
constructor() {
super('walk');
this.speed = -30;
}
update = (entity) => {
// 当前实体的x方向速度是当前的速度
entity.vel.x = this.speed;
}
obstruct = (entity, side) => {
// 如果在x方向碰撞了,反向
if (side === Sides.LEFT || side === Sides.RIGHT) {
this.speed = -this.speed;
}
}
}
新增BoundingBox.js
用于定义某个实体当前所在的位置在其x、y方向上,考虑偏移量的情况下,计算其left、top、bottom、right、center等
import { Vector } from "./math.js";
export default class BoundingBox {
constructor(pos, size, offset) {
this.pos = pos;
this.size = size;
this.offset = offset;
}
overlaps(box) {
return this.bottom > box.top
&& this.top < box.bottom
&& this.left < box.right
&& this.right > box.left;
}
getCenter() {
return new Vector(this.meridian, this.equator);
}
setCenter(vec2) {
this.meridian = vec2.x;
this.equator = vec2.y;
}
get meridian() {
return this.pos.x + this.offset.x + this.size.x / 2;
}
set meridian(c) {
this.pos.x = c - (this.size.x / 2 + this.offset.x);
}
get equator() {
return this.pos.y + this.offset.y + this.size.y / 2;
}
set equator(c) {
this.pos.y = c - (this.size.y / 2 + this.offset.y);
}
get bottom() {
return this.pos.y + this.size.y + this.offset.y;
}
set bottom(y) {
this.pos.y = y - (this.size.y + this.offset.y);
}
get top() {
return this.pos.y + this.offset.y;
}
set top(y) {
this.pos.y = y - this.offset.y;
}
get left() {
return this.pos.x + this.offset.x;
}
set left(x) {
this.pos.x = x - this.offset.x;
}
get right() {
return this.pos.x + this.size.x + this.offset.x;
}
set right(x) {
this.pos.x = x - (this.size.x + this.offset.x);
}
}
新增entities.js
用于简化main函数中create实体的Promise.all方法,批量创建实体并导出到main函数进行使用。
import { loadMario } from "./entities/Mario.js";
import { loadKoopa } from "./entities/Koopa.js";
import { loadGoomba } from "./entities/Goomba.js";
export function LoadEntities() {
const entityFactory = {};
function addAs(name) {
return factory => entityFactory[name] = factory
}
return Promise.all(
[loadMario().then(addAs('mario')),
loadGoomba().then(addAs('goomba')),
loadKoopa().then(addAs('koopa'))])
.then(() => entityFactory);
}
新增sprites/goomba.json
用于定义蘑菇小怪的切片和动画配置
{
"tileImgUrl": "/src/assets/characters.png",
"tileWidth": 16,
"tileHeight": 16,
"frames": [
{
"name": "walk-1",
"rect": [296, 187, 16, 16]
},
{
"name": "walk-2",
"rect": [315, 187, 16, 16]
},
{
"name": "flat",
"rect": [277, 53, 16, 16]
}
],
"animations": [
{
"name": "walk",
"frameLength": 0.15,
"frames": [
"walk-1",
"walk-2"
]
}
]
}
新增sprites/koopa.json
用于定义乌龟小怪的切片和动画配置
{
"tileImgUrl": "/src/assets/characters.png",
"tileWidth": 16,
"tileHeight": 24,
"frames": [
{
"name": "walk-1",
"rect": [296, 206, 16, 24]
},
{
"name": "walk-2",
"rect": [315, 206, 16, 24]
}
],
"animations": [
{
"name": "walk",
"frameLength": 0.15,
"frames": [
"walk-1",
"walk-2"
]
}
]
}
本节不难,希望大家能懂起