文章目录
- 八、角色动作状态的管理
- 8.1 准备部分
- 8.2 角色状态改变的基本方式
- 8.3 完善整个代码
- 8.4 存在的问题
- 九、简单的横板动作卷轴游戏
- 9.1 准备部分
- 9.2 输入管理器
- 9.3 状态管理器
- 9.4 背景管理器
- 9.5 敌人管理器
- 9.6 碰撞检测、UI绘制
- 9.7 更多的角色状态与特效
- 9.8 完善游戏
- 附录
素材可以去一位大佬放在github的源码中直接下,见附录。
八、角色动作状态的管理
游戏做大后,对于角色的状态的管理将十分的复杂。本节学习有限状态机下的角色状态管理。
8.1 准备部分
在使用之前,为了方便我们安装服务器插件,让它模拟我们的网站部署在服务器上的形式。
安装后,右下角可以看到Go Live的标识
在html页面,点击即可完成服务器部署,在此点击该标识即为关闭。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>state management in games</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas1"></canvas>
<img src="./dog.png" alt="dogImage" id="dogImage">
<h1 id="loading">LOADING...</h1>
<script type="module" src="./script.js"></script>
</body>
</html>
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Helvetica','cursive';
}
body{
overflow: hidden;
}
canvas{
top: 0;
left: 0;
}
img{
display: none;
}
#loading{
position: absolute;
top: 50%;
width: 100%;
text-align: center;
font-size: 80px;
}
接下来完成类似如下文件的创建
我们先准备一个较基本的玩家的类player.js
export default class Player{
constructor(gameWidth,gameHeight){
this.gameWidth=gameWidth;
this.gameHeight=gameHeight;
// 存储所有状态
this.states=[];
// 当前状态
this.currentState=this.states[0];
this.image= document.getElementById('dogImage');
this.width=200;
this.height=181.83;
// 初始位置,让狗站在地面上
this.x=this.gameWidth/2-this.width/2
this.y=this.gameHeight/2-this.height/2
// 当前动画帧
this.frameX=0;
this.maxFrame=6;
this.frameY=0;
}
draw(context){
context.drawImage(this.image,this.width*this.frameX,this.height*this.frameY,this.width,this.height,this.x,this.y,this.width,this.height)
}
}
一个输入类input.js
export default class InputHandler {
constructor() {
this.lastKey = '';
window.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowLeft':
this.lastKey = 'Press left';
break;
case 'ArrowRight':
this.lastKey = 'Press right';
break;
case 'ArrowDown':
this.lastKey = 'Press down';
break;
case 'ArrowUp':
this.lastKey = 'Press up';
break;
}
});
window.addEventListener('keyup', e => {
switch (e.key) {
case 'ArrowLeft':
this.lastKey = 'Release left';
break;
case 'ArrowRight':
this.lastKey = 'Release right';
break;
case 'ArrowDown':
this.lastKey = 'Release down';
break;
case 'ArrowUp':
this.lastKey = 'Release up';
break;
}
});
}
}
一个用于输出状态的utils
export function drawStatusText(context,input){
context.font='20px Helvetica';
context.fillText('Last input:' + input.lastKey,30,40);
}
script.js为我们页面加载的js,由他来组织整个游戏
import Player from "./player.js";
import InputHandler from './input.js'
import {drawStatusText} from "./utils.js"
window,addEventListener('load',function(){
const loading = document.getElementById('loading');
loading.style.display='none';
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = this.window.innerHeight;
const player = new Player(canvas.width,canvas.height);
const input = new InputHandler()
function animate(){
ctx.clearRect(0,0,canvas.width,canvas.height)
player.draw(ctx);
drawStatusText(ctx,input);
requestAnimationFrame(animate)
}
animate();
})
8.2 角色状态改变的基本方式
我们接下来实现player中的状态。我们说过因为游戏复杂起来后,角色的状态不好管理,因此我们需要将它单独抽象出来,以此方便管理和拓展。
在该类中,我们会发现,由它根据我们的按键来修改用户类,以此达到让他和用户类解耦。
export const states = {
STANDING_LEFT:0,
STANDING_RIGHT:1,
}
class State{
constructor(state){
this.state=state
}
// 初始化方法
enter(){}
// 处理输入
handleInput(){}
}
export class StandingLeft extends State{
constructor(player){
super('STANDING_LEFT');
this.player = player;
}
enter(){
this.player.frameY = 1;
this.player.maxFrame = 6;
}
handleInput(input){
// 如果不是按下的左键,就反应
if(input === 'Press right'){
this.player.setState(states.STANDING_RIGHT);
}
}
}
export class StandingRight extends State{
constructor(player){
super('STANDING_RIGHT');
this.player = player;
}
enter(){
this.player.frameY = 0;
this.player.maxFrame = 6;
}
handleInput(input){
// 如果不是按下的右键,就反应
if(input === 'Press left'){
this.player.setState(states.STANDING_LEFT);
}
}
}
同时修改player.js
import {StandingLeft,StandingRight} from './state.js';
export default class Player{
constructor(gameWidth,gameHeight){
this.gameWidth = gameWidth;
this.gameHeight = gameHeight;
// 存储所有状态
this.states=[
new StandingLeft(this),
new StandingRight(this)
];
// 当前状态
this.currentState=this.states[1];
this.image= document.getElementById('dogImage');
this.width=200;
this.height=181.83;
// 初始位置,让狗站在地面上
this.x=this.gameWidth/2 - this.width/2;
this.y=this.gameHeight/2 - this.height/2;
// 当前动画帧
this.frameX = 0;
this.maxFrame = 6;
this.frameY = 0;
}
// 绘制方法
draw(context){
context.drawImage(this.image,this.width*this.frameX,this.height*this.frameY,this.width,this.height,this.x,this.y,this.width,this.height)
}
// 更新方法
update(input){
this.currentState.handleInput(input);
}
// 状态设置
setState(state){
this.currentState = this.states[state];
this.currentState.enter();
}
}
随后修改utils.js
export function drawStatusText(context,input,player){
context.font='30px Helvetica';
context.fillText('Last input:' + input.lastKey,20,50);
context.fillText('Active state:' + player.currentState.state,20,90);
}
script.js记得也要修改
//...
function animate(){
ctx.clearRect(0,0,canvas.width,canvas.height)
player.update(input.lastKey);
player.draw(ctx);
drawStatusText(ctx,input,player);
requestAnimationFrame(animate)
}
//...
8.3 完善整个代码
我们补足各个状态。对于跳跃,我们要补上检测是否在地面。
export const states={
STANDING_LEFT:0,
STANDING_RIGHT:1,
SITTING_LEFT:2,
SITTING_RIGHT:3,
RUNNING_LEFT:4,
RUNNING_RIGHT:5,
JUMPING_LEFT:6,
JUMPING_RIGHT:7,
FALLING_LEFT:8,
FALLING_RIGHT:9
}
class State{
constructor(state){
this.state = state;
}
// 初始化方法
enter(){}
// 处理输入
handleInput(){}
}
export class StandingLeft extends State{
constructor(player){
super('STANDING_LEFT');
this.player = player;
}
enter(){
this.player.frameY = 1;
this.player.speed = 0;
this.player.maxFrame = 6;
}
handleInput(input){
if(input==='Press right')
this.player.setState(states.RUNNING_RIGHT);
else if(input==='Press left')
this.player.setState(states.RUNNING_LEFT);
else if(input==='Press down')
this.player.setState(states.SITTING_LEFT);
else if(input==='Press up')
this.player.setState(states.JUMPING_LEFT);
}
}
export class StandingRight extends State{
constructor(player){
super('STANDING_RIGHT');
this.player = player;
}
enter(){
this.player.frameY = 0;
this.player.speed = 0;
this.player.maxFrame = 6;
}
handleInput(input){
if(input==='Press left')
this.player.setState(states.RUNNING_LEFT);
else if(input==='Press right')
this.player.setState(states.RUNNING_RIGHT);
else if(input==='Press down')
this.player.setState(states.SITTING_RIGHT);
else if(input==='Press up')
this.player.setState(states.JUMPING_RIGHT);
}
}
export class SittingLeft extends State{
constructor(player){
super('SITTING_LEFT');
this.player = player;
}
enter(){
this.player.frameY = 9;
this.player.speed = 0;
this.player.maxFrame = 4;
}
handleInput(input){
if(input==='Press right')
this.player.setState(states.SITTING_RIGHT);
else if(input==='Release down')
this.player.setState(states.STANDING_LEFT);
}
}
export class SittingRight extends State{
constructor(player){
super('SITTING_RIGHT');
this.player = player;
}
enter(){
this.player.frameY = 8;
this.player.speed = 0;
this.player.maxFrame = 4;
}
handleInput(input){
if(input==='Press left')
this.player.setState(states.SITTING_LEFT);
else if(input==='Release down')
this.player.setState(states.STANDING_RIGHT);
}
}
export class RunningLeft extends State{
constructor(player){
super('RUNNING_LEFT');
this.player = player;
}
enter(){
this.player.frameY = 7;
this.player.speed = -this.player.maxSpeed;
this.player.maxFrame = 8;
}
handleInput(input){
if(input==='Press right')
this.player.setState(states.RUNNING_RIGHT);
else if(input==='Release left')
this.player.setState(states.STANDING_LEFT);
else if(input==='Press down')
this.player.setState(states.SITTING_LEFT);
}
}
export class RunningRight extends State{
constructor(player){
super('RUNNING_RIGHT')
this.player=player
}
enter(){
this.player.frameY=6
this.player.speed=this.player.maxSpeed
this.player.maxFrame=8
}
handleInput(input){
if(input==='Press left') this.player.setState(states.RUNNING_LEFT)
else if(input==='Release right') this.player.setState(states.STANDING_RIGHT)
else if(input==='Press down') this.player.setState(states.SITTING_RIGHT)
}
}
export class JumpingLeft extends State{
constructor(player){
super('JUMPING_LEFT');
this.player = player;
}
enter(){
this.player.frameY = 3;
if(this.player.onGround())
this.player.vy -= 10;
this.player.speed = -this.player.maxSpeed * 0.5;
this.player.maxFrame = 6;
}
handleInput(input){
if(input==='Press right')
this.player.setState(states.JUMPING_RIGHT);
else if(this.player.onGround())
this.player.setState(states.STANDING_LEFT);
}
}
export class JumpingRight extends State{
constructor(player){
super('JUMPING_RIGHT');
this.player = player;
}
enter(){
this.player.frameY = 2;
if(this.player.onGround())
this.player.vy -= 10;
this.player.speed = this.player.maxSpeed * 0.5;
this.player.maxFrame = 6;
}
handleInput(input){
if(input==='Press left')
this.player.setState(states.JUMPING_LEFT);
else if(this.player.onGround())
this.player.setState(states.STANDING_RIGHT);
}
}
export class FallingLeft extends State{
constructor(player){
super('FALLING_LEFT')
this.player = player;
}
enter(){
this.player.frameY = 5;
}
handleInput(input){
if(input==='Press right')
this.player.setState(states.FALLING_RIGHT);
else if(this.player.onGround())
this.player.setState(states.STANDING_LEFT);
else if(this.player.vy > 0)
this.player.setState(states.FALLING_LEFT);
}
}
export class FallingRight extends State{
constructor(player){
super('FALLING_RIGHT')
this.player = player;
}
enter(){
this.player.frameY = 4;
}
handleInput(input){
if(input==='Press left')
this.player.setState(states.FALLING_LEFT);
else if(this.player.onGround())
this.player.setState(states.STANDING_RIGHT);
else if(this.player.vy > 0)
this.player.setState(states.FALLING_RIGHT);
}
}
接着补全player类
我们先完善角色的代码。
```js
import {StandingLeft,StandingRight,SittingLeft,SittingRight,RunningLeft,RunningRight,JumpingLeft,JumpingRight,FallingLeft,FallingRight} from './state.js';
export default class Player{
constructor(gameWidth,gameHeight){
this.gameWidth = gameWidth;
this.gameHeight = gameHeight;
// 存储所有状态
this.states=[
new StandingLeft(this),
new StandingRight(this),
new SittingLeft(this),
new SittingRight(this),
new RunningLeft(this),
new RunningRight(this),
new JumpingLeft(this),
new JumpingRight(this),
new FallingLeft(this),
new FallingRight(this)
];
// 当前状态
this.currentState=this.states[1];
this.image= document.getElementById('dogImage');
this.width=200;
this.height=181.83;
// 初始位置,让狗站在地面上
this.x=this.gameWidth/2 - this.width/2;
this.y=this.gameHeight/2 - this.height/2;
// 当前动画帧
this.frameX = 0;
this.maxFrame = 6;
this.frameY = 0;
// 下落速度
this.vy = 0;
// 加速度
this.weight = 0.5;
// 水平速度
this.speed = 0;
// 最大速度
this.maxSpeed = 10;
// 帧数
this.fps = 30;
// 累计经过时间
this.frameTimer = 0;
// 实际间隔
this.frameInterval= 1000/this.fps;
}
// 绘制方法
draw(context,deltaTime){
if(this.frameTimer > this.frameInterval){
if(this.frameX < this.maxFrame)
this.frameX++;
else
this.frameX = 0;
this.frameTimer = 0;
}else{
this.frameTimer += deltaTime;
}
context.drawImage(this.image,this.width*this.frameX,this.height*this.frameY,this.width,this.height,this.x,this.y,this.width,this.height)
}
// 更新方法
update(input){
this.currentState.handleInput(input)
this.x += this.speed;
if(this.x <= 0)
this.x = 0;
else if(this.x>=this.gameWidth-this.width)
this.x = this.gameWidth - this.width;
this.y += this.vy;
if(!this.onGround()){
this.vy += this.weight;
}else{
this.vy = 0;
}
if(this.y > this.gameHeight - this.height)
this.y=this.gameHeight-this.height;
}
// 状态设置
setState(state){
this.currentState = this.states[state];
this.currentState.enter();
}
// 地面检测
onGround(){
return this.y >= this.gameHeight - this.height;
}
}
最后完善script.js
import Player from "./player.js";
import InputHandler from './input.js'
import {drawStatusText} from "./utils.js"
window,addEventListener('load',function(){
const loading = document.getElementById('loading');
loading.style.display='none';
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = this.window.innerHeight;
const player = new Player(canvas.width,canvas.height);
const input = new InputHandler()
let lastTime = 0;
function animate(timeStamp){
const deltaTime=timeStamp-lastTime
lastTime=timeStamp
ctx.clearRect(0,0,canvas.width,canvas.height)
player.update(input.lastKey)
player.draw(ctx,deltaTime)
drawStatusText(ctx,input,player)
requestAnimationFrame(animate)
}
animate(0)
})
8.4 存在的问题
该版本只是简单的样例,接收单按键,通过改为数组或其他方式记录同时按下的键位来允许多按键操作。
九、简单的横板动作卷轴游戏
本节是该课程最后一节。利用前面我们所习得的编码内容,完成一款小游戏的制作。
9.1 准备部分
请使用第八节中的插件,让该项目运行在服务器中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas1"></canvas>
<img src="./player.png" alt="" id="player">
<img src="./layer-1.png" alt="" id="layer1">
<img src="./layer-2.png" alt="" id="layer2">
<img src="./layer-3.png" alt="" id="layer3">
<img src="./layer-4.png" alt="" id="layer4">
<img src="./layer-5.png" alt="" id="layer5">
<img src="./enemy_fly.png" alt="" id="enemy_fly">
<img src="./enemy_plant.png" alt="" id="enemy_plant">
<img src="./enemy_spider_big.png" alt="" id="enemy_spider_big">
<img src="./fire.png" alt="" id="fire">
<img src="./boom.png" alt="" id="collisionAnimation">
<img src="./heart.png" alt="" id="lives">
<script type="module" src="main.js"></script>
</body>
</html>
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
#canvas1{
border: 5px solid black;
position: absolute;
top: 50%;
left:50%;
transform: translate(-50%,-50%);
max-width: 100%;
max-height: 100%;
font-family: 'Creepster', cursive;
}
#player,#layer1,#layer2,#layer3,#layer4,#layer5,#enemy_fly,#enemy_plant,#enemy_spider_big,#fire,#collisionAnimation,#lives{
display: none;
}
9.2 输入管理器
我们准备好输入处理器
类似第八部分的写法,不过我们这里使用数组来允许多个按键
export default class InputHandler{
constructor(game){
this.game = game;
this.keys = [];
window.addEventListener('keydown',e=>{
if((e.key === 'ArrowDown' ||
e.key === 'ArrowUp'||
e.key === 'ArrowLeft'||
e.key === 'ArrowRight'||
e.key === 'Enter'
)
&&this.keys.indexOf(e.key) === -1){
this.keys.push(e.key)
}
})
window.addEventListener('keyup',e=>{
if(e.key === 'ArrowDown'||
e.key === 'ArrowUp'||
e.key === 'ArrowLeft'||
e.key === 'ArrowRight'||
e.key === 'Enter')
{
this.keys.splice(this.keys.indexOf(e.key),1)
}
})
}
}
用户类
export default class Player{
constructor(game){
this.game = game;
this.width = 100;
this.height = 91.3;
this.x = 0;
this.y = this.game.height - this.height;
this.image = document.getElementById('player');
}
update(input){
if(input.includes('ArrowRight')){
this.x++;
}
else if(input.includes('ArrowLeft')){
this.x--;
}
}
draw(context){
context.drawImage(this.image,0,0,this.width,this.height,this.x,this.y,this.width,this.height);
}
}
main.js
import Player from './player.js'
import InputHandler from './input.js'
window.addEventListener('load', function () {
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas.height = 500;
class Game{
constructor(width,height){
this.width = width;
this.height = height;
this.player = new Player(this);
this.input = new InputHandler(this);
}
update(){
this.player.update(this.input.keys);
}
draw(context){
this.player.draw(context);
}
}
const game = new Game(canvas.width,canvas.height);
function animate(){
ctx.clearRect(0,0,canvas.width,canvas.height);
game.update();
game.draw(ctx);
requestAnimationFrame(animate);
}
animate();
});
当然,我们可以进一步完善,加入跳跃等部分。
export default class Player{
constructor(game){
this.game = game;
this.width = 100;
this.height = 91.3;
this.x = 0;
this.y = this.game.height - this.height;
this.image = document.getElementById('player');
this.speed = 0;
this.maxSpeed = 10;
this.vy = 0;
this.weight = 1;
}
update(input){
// 水平方向
this.x += this.speed;
if(input.includes('ArrowRight')){
this.speed = this.maxSpeed;
}
else if(input.includes('ArrowLeft')){
this.speed = -this.maxSpeed;
}
else{
this.speed = 0;
}
if(this.x < 0){
this.x = 0;
}
if(this.x > this.game.width - this.width){
this.x = this.game.width - this.width;
}
// 竖直方向
if(input.includes('ArrowUp') && this.onGround()){
this.vy -= 20;
}
this.y += this.vy;
if(!this.onGround()){
this.vy += this.weight;
}
else{
this.vy = 0;
}
}
draw(context){
context.drawImage(this.image,0,0,this.width,this.height,this.x,this.y,this.width,this.height);
}
onGround(){
return this.y >= this.game.height - this.height;
}
}
9.3 状态管理器
我们接下来加入第八节中的状态管理器。
同时加入fps和动画帧
const states={
SITTING: 0,
RUNNING: 1,
JUMPING: 2,
FALLING: 3,
ROLLING: 4,
DIVING: 5,
HIT: 6
}
class State{
constructor(state){
this.state = state;
}
enter(){}
handleInput(input){}
}
export class Sitting extends State{
constructor(player){
super('SITTING');
this.player = player;
}
enter(){
this.player.frameX = 0;
this.player.frameY = 5;
this.player.maxFrame = 4;
}
handleInput(input){
if(input.includes('ArrowLeft') || input.includes('ArrowRight')){
this.player.setState(states.RUNNING);
}else if(input.includes('ArrowUp')){
this.player.setState(states.JUMPING);
}
}
}
export class Running extends State{
constructor(player){
super('RUNNING');
this.player = player;
}
enter(){
this.player.frameX = 0;
this.player.frameY = 3;
this.player.maxFrame = 6;
}
handleInput(input){
if(input.includes('ArrowDown')){
this.player.setState(states.SITTING);
}else if(input.includes('ArrowUp')){
this.player.setState(states.JUMPING);
}
}
}
export class Jumping extends State{
constructor(player){
super('JUMPING');
this.player = player;
}
enter(){
if(this.player.onGround()){
this.player.vy -= 20;
}
this.player.frameX = 0;
this.player.frameY = 1;
this.player.maxFrame = 6;
}
handleInput(input){
if(this.player.vy > this.player.weight){
this.player.setState(states.FALLING);
}
}
}
export class Falling extends State{
constructor(player){
super('FALLING');
this.player = player;
}
enter(){
this.player.frameX = 0;
this.player.frameY = 2;
this.player.maxFrame = 6;
}
handleInput(input){
if(this.player.onGround()){
this.player.setState(states.RUNNING);
}
}
}
import { Sitting,Running,Jumping,Falling } from "./playerState.js";
export default class Player{
constructor(game){
this.game = game;
this.width = 100;
this.height = 91.3;
this.x = 0;
this.y = this.game.height - this.height;
this.image = document.getElementById('player');
this.speed = 0;
this.maxSpeed = 10;
this.vy = 0;
this.weight = 1;
this.states = [
new Sitting(this),
new Running(this),
new Jumping(this),
new Falling(this),
];
this.currentState = this.states[0];
this.currentState.enter();
this.frameX = 0;
this.frameY = 0;
this.maxFrame = 5;
this.fps = 20;
this.frameInterval = 1000/this.fps;
this.frameTimer = 0;
}
update(input,deltaTime){
// 处理输入
this.currentState.handleInput(input);
// 水平方向
this.x += this.speed;
if(input.includes('ArrowRight')){
this.speed = this.maxSpeed;
}
else if(input.includes('ArrowLeft')){
this.speed = -this.maxSpeed;
}
else{
this.speed = 0;
}
if(this.x < 0){
this.x = 0;
}
if(this.x > this.game.width - this.width){
this.x = this.game.width - this.width;
}
// 竖直方向
this.y += this.vy;
if(!this.onGround()){
this.vy += this.weight;
}
else{
this.vy = 0;
}
if(this.frameTimer > this.frameInterval){
this.frameTimer = 0;
if(this.frameX < this.maxFrame){
this.frameX++;
}
else{
this.frameX = 0;
}
}else{
this.frameTimer += deltaTime;
}
}
draw(context){
context.drawImage(this.image,this.frameX*this.width,this.frameY*this.height,this.width,this.height,this.x,this.y,this.width,this.height);
}
onGround(){
return this.y >= this.game.height - this.height;
}
setState(state){
this.currentState = this.states[state];
this.currentState.enter();
}
}
import Player from './player.js'
import InputHandler from './input.js'
window.addEventListener('load', function () {
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas.height = 500;
class Game{
constructor(width,height){
this.width = width;
this.height = height;
this.player = new Player(this);
this.input = new InputHandler(this);
}
update(deltaTime){
this.player.update(this.input.keys,deltaTime);
}
draw(context){
this.player.draw(context);
}
}
const game = new Game(canvas.width,canvas.height);
let lastTime = 0;
function animate(timeStamp){
let deltaTime = timeStamp - lastTime;
lastTime = timeStamp;
ctx.clearRect(0,0,canvas.width,canvas.height);
game.update(deltaTime);
game.draw(ctx);
requestAnimationFrame(animate);
}
animate(0);
});
9.4 背景管理器
我们先添加地面的margin和游戏速度
class Game{
constructor(width,height){
this.width = width;
this.height = height;
this.groundMargin = 50;
this.speed = 3;
this.player = new Player(this);
this.input = new InputHandler(this);
}
update(deltaTime){
this.player.update(this.input.keys,deltaTime);
}
draw(context){
this.player.draw(context);
}
}
同时修改玩家类
export default class Player{
constructor(game){
//...
this.y = this.game.height - this.height - this.game.groundMargin;
//...
}
//...
onGround(){
return this.y >= this.game.height - this.height - this.game.groundMargin;
}
}
background.js
class Layer{
constructor(game,width,height,speedModifier,image){
this.game = game;
this.width = width;
this.height = height;
this.speedModifier = speedModifier;
this.image = image;
this.x = 0;
this.y = 0;
}
update(){
if(this.x < -this.width){
this.x = 0;
}
else{
this.x -= this.game.speed * this.speedModifier;
}
}
draw(context){
context.drawImage(this.image,this.x,this.y,this.width,this.height);
context.drawImage(this.image,this.x + this.width,this.y,this.width,this.height);
}
}
export class Background{
constructor(game){
this.game = game;
this.width = 1667;
this.height = 500;
this.layer5image = document.getElementById('layer5');
this.layer1 = new Layer(this.game,this.width,this.height,1,this.layer5image);
this.backgroundLayers = [
this.layer1,
];
}
update(){
this.backgroundLayers.forEach(e=>{
e.update();
})
}
draw(context){
this.backgroundLayers.forEach(e=>{
e.draw(context);
})
}
}
import Player from './player.js'
import InputHandler from './input.js'
import {Background }from './background.js'
window.addEventListener('load', function () {
//...
class Game{
constructor(width,height){
this.width = width;
this.height = height;
this.groundMargin = 50;
this.speed = 0;
this.maxSpeed = 3;
this.background = new Background(this);
this.player = new Player(this);
this.input = new InputHandler(this);
}
update(deltaTime){
this.background.update();
this.player.update(this.input.keys,deltaTime);
}
draw(context){
this.background.draw(context);
this.player.draw(context);
}
}
//...
接下来加入更多的背景
class Layer{
constructor(game,width,height,speedModifier,image){
this.game = game;
this.width = width;
this.height = height;
this.speedModifier = speedModifier;
this.image = image;
this.x = 0;
this.y = 0;
}
update(){
if(this.x < -this.width){
this.x = 0;
}
else{
this.x -= this.game.speed * this.speedModifier;
}
}
draw(context){
context.drawImage(this.image,this.x,this.y,this.width,this.height);
context.drawImage(this.image,this.x + this.width,this.y,this.width,this.height);
}
}
export class Background{
constructor(game){
this.game = game;
this.width = 1667;
this.height = 500;
this.layer1image = document.getElementById('layer1');
this.layer2image = document.getElementById('layer2');
this.layer3image = document.getElementById('layer3');
this.layer4image = document.getElementById('layer4');
this.layer5image = document.getElementById('layer5');
this.layer1 = new Layer(this.game,this.width,this.height,0,this.layer1image);
this.layer2 = new Layer(this.game,this.width,this.height,0.2,this.layer2image);
this.layer3 = new Layer(this.game,this.width,this.height,0.4,this.layer3image);
this.layer4 = new Layer(this.game,this.width,this.height,0.8,this.layer4image);
this.layer5 = new Layer(this.game,this.width,this.height,1,this.layer5image);
this.backgroundLayers = [
this.layer1,
this.layer2,
this.layer3,
this.layer4,
this.layer5,
];
}
update(){
this.backgroundLayers.forEach(e=>{
e.update();
})
}
draw(context){
this.backgroundLayers.forEach(e=>{
e.draw(context);
})
}
}
我们接下来修改部分方法,让背景的速度与角色有一定关联
export default class Player{
//...
setState(state,speed){
this.game.speed = speed * this.game.maxSpeed;
this.currentState = this.states[state];
this.currentState.enter();
}
//...
}
//...
export class Sitting extends State{
constructor(player){
super('SITTING');
this.player = player;
}
enter(){
this.player.frameX = 0;
this.player.frameY = 5;
this.player.maxFrame = 4;
}
handleInput(input){
if(input.includes('ArrowLeft') || input.includes('ArrowRight')){
this.player.setState(states.RUNNING,1);
}else if(input.includes('ArrowUp')){
this.player.setState(states.JUMPING,1);
}
}
}
export class Running extends State{
constructor(player){
super('RUNNING');
this.player = player;
}
enter(){
this.player.frameX = 0;
this.player.frameY = 3;
this.player.maxFrame = 6;
}
handleInput(input){
if(input.includes('ArrowDown')){
this.player.setState(states.SITTING,0);
}else if(input.includes('ArrowUp')){
this.player.setState(states.JUMPING,1);
}
}
}
export class Jumping extends State{
constructor(player){
super('JUMPING');
this.player = player;
}
enter(){
if(this.player.onGround()){
this.player.vy -= 20;
}
this.player.frameX = 0;
this.player.frameY = 1;
this.player.maxFrame = 6;
}
handleInput(input){
if(this.player.vy > this.player.weight){
this.player.setState(states.FALLING,1);
}
}
}
export class Falling extends State{
constructor(player){
super('FALLING');
this.player = player;
}
enter(){
this.player.frameX = 0;
this.player.frameY = 2;
this.player.maxFrame = 6;
}
handleInput(input){
if(this.player.onGround()){
this.player.setState(states.RUNNING,1);
}
}
}
9.5 敌人管理器
我们创建一个enemy,js
class Enemy{
constructor(game){
this.frameX = 0;
this.frameY = 0;
this.game = game;
this.fps = 20;
this.frameInterval = 1000/this.fps;
this.frameTimer = 0;
this.markedForDeletion=false;
}
update(deltaTime){
this.x -= this.speedX + this.game.speed
this.y += this.speedY;
if(this.frameTimer > this.frameInterval){
this.frameTimer = 0;
if(this.frameX<this.maxFrame)
this.frameX++;
else{
this.frameX = 0;
}
}
else{
this.frameTimer += deltaTime;
}
if(this.x + this.width<0){
this.markedForDeletion = true
}
}
draw(context){
context.drawImage(this.image,this.frameX*this.width,0,this.width,this.height,this.x,this.y,this.width,this.height)
}
}
export class FlyingEnemy extends Enemy{
constructor(game){
super()
this.game = game;
this.width = 60;
this.height = 44;
this.x = this.game.width;
this.y = Math.random() * this.game.height * 0.5;
this.speedX = Math.random() + 1;
this.speedY = 0;
this.maxFrame = 5;
this.image = document.getElementById('enemy_fly');
this.angle = 0;
this.va = Math.random() * 0.1 + 0.1;
}
update(deltaTime){
super.update(deltaTime);
this.angle += this.va;
this.y += Math.sin(this.angle);
}
}
export class GroundEnemy extends Enemy{
constructor(game){
super();
this.game = game;
this.width = 60;
this.height = 87;
this.x = this.game.width;
this.y = this.game.height - this.height - this.game.groundMargin;
this.speedX = 0;
this.speedY = 0;
this.maxFrame = 1;
this.image = document.getElementById('enemy_plant');
}
}
export class ClimbingEnemy extends Enemy{
constructor(game){
super();
this.game = game;
this.width = 120;
this.height = 144;
this.x = this.game.width;
this.y = Math.random() * this.game.height * 0.5;
this.image = document.getElementById('enemy_spider_big');
this.speedX = 0;
this.speedY = Math.random() > 0.5 ? 1 : -1;
this.maxFrame = 5;
}
update(deltaTime){
super.update(deltaTime);
if(this.y > this.game.height - this.height - this.game.groundMargin){
this.speedY *= -1
}
if(this.y < - this.height){
this.markedForDeletion = true;
}
}
draw(context){
super.draw(context);
context.beginPath();
context.moveTo(this.x+this.width/2,0);
context.lineTo(this.x+this.width/2,this.y);
context.stroke();
}
}
最后在main.js的游戏类中,加入怪物
class Game{
constructor(width,height){
//...
this.enemies = [];
this.enemyTimer = 0;
this.enemyInterval = 1000;
}
addEnemy(){
if (this.speed > 0 && Math.random() < 0.5){
this.enemies.push(new GroundEnemy(this))
}
else if (this.speed > 0){
this.enemies.push(new ClimbingEnemy(this))
}
this.enemies.push(new FlyingEnemy(this))
}
update(deltaTime){
this.background.update();
this.player.update(this.input.keys,deltaTime);
if(this.enemyTimer > this.enemyInterval){
this.addEnemy();
this.enemyTimer = 0;
}
else{
this.enemyTimer += deltaTime;
}
this.enemies.forEach((e)=>{
e.update(deltaTime);
if(e.markedForDeletion){
this.enemies.splice(this.enemies.indexOf(e),1)
}
});
}
draw(context){
this.background.draw(context);
this.player.draw(context);
this.enemies.forEach(e=>{
e.draw(context);
})
this.UI.draw(context);
}
}
9.6 碰撞检测、UI绘制
我们首先更新一个debug模式,该模式下会绘画出碰撞盒。同时准备一下计分等信息。
class Game{
constructor(width,height){
//...
this.debug = false;
this.score = 0;
this.fontColor = 'black';
// 稍后创建
this.UI = new UI(this);
}
draw(context){
//...
// 稍后创建
this.UI.draw(context);
}
}
export default class InputHandler{
constructor(game){
//...
window.addEventListener('keydown',e=>{
if((e.key === 'ArrowDown' ||
e.key === 'ArrowUp'||
e.key === 'ArrowLeft'||
e.key === 'ArrowRight'||
e.key === 'Enter'
)
&&this.keys.indexOf(e.key) === -1){
this.keys.push(e.key)
}
else if(e.key==='d'){
this.game.debug = !this.game.debug;
}
})
//...
}
}
class Enemy{
//...
draw(context){
if(this.game.debug){
context.strokeRect(this.x,this.y,this.width,this.height);
}
context.drawImage(this.image,this.frameX*this.width,0,this.width,this.height,this.x,this.y,this.width,this.height)
}
}
我们先写一个简单的碰撞检测
export default class Player{
//...
update(input,deltaTime){
this.checkCollision();
}
checkCollision(){
this.game.enemies.forEach(enemy => {
if(enemy.x < this.x + this.width &&
enemy.x + enemy.width > this.x &&
enemy.y < this.y + this.height &&
enemy.y+enemy.height > this.y)
{
enemy.markedForDeletion = true
this.game.score++;
}
else{
}
});
}
}
我们接下来创建一个UI类
export class UI{
constructor(game){
this.game = game;
this.fontSize = 28;
this.fontFamily = 'Helvetica';
}
draw(context){
context.save();
context.font = this.fontSize+'px ' + this.fontFamily;
context.textAlign = 'left'
context.fillStyle = this.game.fontColor
context.fillText('Score: '+this.game.score,20,50)
context.restore()
}
}
9.7 更多的角色状态与特效
我们需要修改playerState.js因为我们不仅需要加入更多的状态,还需要加入特效
//特效类,后面创建
import {Dust,Fire,Splash} from './particles.js'
const states = {
SITTING: 0,
RUNNING: 1,
JUMPING: 2,
FALLING: 3,
ROLLING: 4,
DIVING: 5,
HIT: 6
}
class State{
constructor(state,game){
this.state = state;
this.game = game;
}
enter(){}
handleInput(input){}
}
export class Sitting extends State{
constructor(game){
super('SITTING',game);
}
enter(){
this.game.player.frameX = 0;
this.game.player.frameY = 5;
this.game.player.maxFrame = 4;
}
handleInput(input){
if(input.includes('ArrowLeft') || input.includes('ArrowRight')){
this.game.player.setState(states.RUNNING,1);
}else if(input.includes('ArrowUp')){
this.game.player.setState(states.JUMPING,1);
}
}
}
export class Running extends State{
constructor(game){
super('RUNNING',game);
}
enter(){
this.game.player.frameX = 0;
this.game.player.frameY = 3;
this.game.player.maxFrame = 6;
}
handleInput(input){
this.game.particles.unshift(new Dust(this.game,this.game.player.x,this.game.player.y));
if(input.includes('ArrowDown')){
this.game.player.setState(states.SITTING,0);
}else if(input.includes('ArrowUp')){
this.game.player.setState(states.JUMPING,1);
}else if(input.includes('Enter')){
this.game.player.setState(states.ROLLING,2);
}
}
}
export class Jumping extends State{
constructor(game){
super('JUMPING',game);
}
enter(){
if(this.game.player.onGround()){
this.game.player.vy -= 20;
}
this.game.player.frameX = 0;
this.game.player.frameY = 1;
this.game.player.maxFrame = 6;
}
handleInput(input){
if(this.game.player.vy > this.game.player.weight){
this.game.player.setState(states.FALLING,1);
}
else if(input.includes('Enter')){
this.game.player.setState(states.ROLLING,2)
}
else if(input.includes('ArrowDown')){
this.game.player.setState(states.DIVING,0)
}
}
}
export class Falling extends State{
constructor(game){
super('FALLING',game);
}
enter(){
this.game.player.frameX = 0;
this.game.player.frameY = 2;
this.game.player.maxFrame = 6;
}
handleInput(input){
if(this.game.player.onGround()){
this.game.player.setState(states.RUNNING,1);
}
}
}
export class Rolling extends State{
constructor(game){
super('ROLLING',game);
}
enter(){
this.game.player.frameX = 0;
this.game.player.frameY = 6;
this.game.player.maxFrame = 6;
}
handleInput(input){
this.game.particles.unshift(new Fire(this.game,this.game.player.x + this.game.player.width*0.5,this.game.player.y + this.game.player.height * 0.5));
if(!input.includes('Enter') && this.game.player.onGround()){
this.game.player.setState(states.RUNNING,1);
}else if(!input.includes("Enter") && !this.game.player.onGround()){
this.game.player.setState(states.FALLING,1);
}else if(input.includes('Enter') && input.includes("ArrowUp")&&this.game.player.onGround()){
this.game.player.vy -= 27;
}else if(input.includes('ArrowDown')&&!this.game.player.onGround()){
this.game.player.setState(states.DIVING,0)
}
}
}
export class Diving extends State{
constructor(game){
super('Diving',game);
}
enter(){
this.game.player.frameX = 0;
this.game.player.frameY = 6;
this.game.player.maxFrame = 6;
this.game.player.vy = 15;
}
handleInput(input){
this.game.particles.unshift(new Fire(this.game,this.game.player.x + this.game.player.width*0.5,this.game.player.y + this.game.player.height * 0.5));
if(this.game.player.onGround()){
this.game.player.setState(states.RUNNING,1);
for(let i = 0;i < 30;i++){
this.game.particles.unshift(new Splash(this.game,this.game.player.x + this.game.player.width*0.5,this.game.player.y + this.game.player.height * 0.5));
}
}else if(input.includes("Enter") && this.game.player.onGround()){
this.game.player.setState(states.ROLLING,2);
}
}
}
export class Hit extends State{
constructor(game){
super('Hit',game);
}
enter(){
this.game.player.frameX = 0;
this.game.player.frameY = 4;
this.game.player.maxFrame = 10;
}
handleInput(input){
if(this.game.player.frameX >= 10 && this.game.player.onGround()){
this.game.player.setState(states.RUNNING,1);
}else if(this.game.player.frameX >= 10 && !this.game.player.onGround()){
this.game.player.setState(states.FALLING,1);
}
}
}
player.js
export default class Player{
constructor(game){
//...
this.states = [
new Sitting(this),
new Running(this),
new Jumping(this),
new Falling(this),
new Rolling(this),
new Diving(this),
new Hit(this)
];
//...
}
//...
}
由于我们传入的变为了game,我们需要将player.js改动
export default class Player{
constructor(game){
//...
this.states = [
new Sitting(this.game),
new Running(this.game),
new Jumping(this.game),
new Falling(this.game),
new Rolling(this.game),
new Diving(this.game),
new Hit(this.game)
];
// 注释掉
// this.currentState = this.states[0];
// this.currentState.enter();
//...
}
}
由于上述改动,我们还需要把原本在player中初始化的部分拿到main.js中初始化
window.addEventListener('load', function () {
//...
class Game{
constructor(width,height){
//...
this.player.currentState = this.player.states[0];
this.player.currentState.enter();
// 特效数组,存储特效
this.particles = [];
}
update(deltaTime){
this.background.update();
this.player.update(this.input.keys,deltaTime);
if(this.enemyTimer > this.enemyInterval){
this.addEnemy();
this.enemyTimer = 0;
}
else{
this.enemyTimer += deltaTime;
}
this.enemies.forEach((e)=>{
e.update(deltaTime);
if(e.markedForDeletion){
this.enemies.splice(this.enemies.indexOf(e),1)
}
});
this.particles.forEach((e)=>{
e.update();
if(e.markedForDeletion){
this.particles.splice(this.particles.indexOf(e),1)
}
});
}
draw(context){
this.background.draw(context);
this.player.draw(context);
this.enemies.forEach(e=>{
e.draw(context);
})
this.particles.forEach(e=>{
e.draw(context);
})
this.UI.draw(context);
}
}
}
接下来我们创建特效类
class Particle{
constructor(game){
this.game = game;
this.markedForDeletion = false;
}
update(){
this.x -= this.speedX + this.game.speed;
this.y -= this.speedY + this.game.speed;
this.size *= 0.95;
if(this.size < 0.5){
this.markedForDeletion = true;
}
}
}
export class Dust extends Particle{
constructor(game,x,y){
super(game);
this.size = Math.random() * 10 + 10;
this.x = x;
this.y = y;
this.speedX = Math.random();
this.speedY = Math.random();
this.color = 'black';
}
draw(context){
context.save();
context.beginPath();
context.arc(this.x,this.y,this.size,0,Math.PI*2);
context.fillStyle = this.color;
context.fill();
context.restore();
}
}
export class Fire extends Particle{
constructor(game,x,y){
super(game);
this.image = document.getElementById('fire');
this.size = Math.random() * 100 + 50;
this.x = x;
this.y = y;
this.speedX = 1;
this.speedY = 1;
this.angel = 0;
this.va = Math.random() * 0.2 - 0.1;
}
update(){
super.update();
this.angel += this.va;
this.x += Math.sin(this.angel*10);
}
draw(context){
context.save();
context.translate(this.x,this.y);
context.rotate(this.angel);
context.drawImage(this.image,-this.size*0.5,-this.size*0.5,this.size,this.size);
context.restore();
}
}
export class Splash extends Particle{
constructor(game,x,y){
super(game)
this.size=Math.random()*100+100
this.x=x-this.size*0.4
this.y=y-this.size*0.5
this.speedX=Math.random()*6-4
this.speedY=Math.random()*2+1
this.gravity=0
this.image=document.getElementById('fire')
}
update(){
super.update()
this.gravity+=0.1
this.y+=this.gravity
}
draw(context){
context.drawImage(this.image,this.x,this.y,this.size,this.size)
}
}
9.8 完善游戏
碰撞动画 collisionAnimation.js
export class CollisionAnimation{
constructor(game,x,y){
this.game = game;
this.image = document.getElementById('collisionAnimation');
this.spritWidth = 100;
this.spritHeight = 90;
this.sizeModifier = Math.random() + 0.5;
this.width = this.sizeModifier * this.spritWidth;
this.height = this.sizeModifier * this.spritHeight;
this.x = x - this.width*0.5;
this.y = y - this.height*0.5;
this.frameX = 0;
this.maxFrame = 4;
this.markedForDeletion = false;
this.fps = Math.random() * 10 + 5;
this.frameInterval = 1000/this.fps;
this.frameTimer = 0;
}
draw(context){
context.drawImage(this.image,this.frameX*this.spritWidth,0,this.spritWidth,this.spritHeight,this.x,this.y,this.width,this.height);
}
update(deltaTime) {
this.x -= this.game.speed;
if(this.frameTimer>this.frameInterval){
this.frameX++;
this.frameTimer = 0;
}else{
this.frameTimer += deltaTime;
}
if(this.frameX > this.maxFrame){
this.markedForDeletion = true;
}
}
}
我们接着在main中加入一些新的变量
window.addEventListener('load', function () {
//...
class Game{
constructor(width,height){
//...
// 碰撞效果
this.collisions = [];
// 得分效果
this.floatingMessages = [];
//生命值
this.lives = 5;
// 游戏时间
this.time = 0;
// 最大时间
this.maxTime = 2000;
this.gameOVer = false;
// 获胜得分
this.winningScore = 20;
}
update(deltaTime){
this.time += deltaTime;
if(this.time > this.maxTime){
this.gameOver = true;
}
//...
this.collisions.forEach(e=>{
e.update(deltaTime);
if(e.markedForDeletion){
this.collisions.splice(this.collisions.indexOf(e),1)
}
});
this.floatingMessages.forEach(e=>{
e.update(deltaTime);
if(e.markedForDeletion){
this.floatingMessages.splice(this.floatingMessages.indexOf(e),1)
}
});
}
draw(context){
this.background.draw(context);
this.player.draw(context);
this.enemies.forEach(e=>{
e.draw(context);
});
this.particles.forEach(e=>{
e.draw(context);
});
this.collisions.forEach(e=>{
e.draw(context);
});
this.floatingMessages.forEach(e=>{
e.draw(context);
});
this.UI.draw(context);
}
}
//...
function animate(timeStamp){
let deltaTime = timeStamp - lastTime;
lastTime = timeStamp;
ctx.clearRect(0,0,canvas.width,canvas.height);
game.update(deltaTime);
game.draw(ctx);
if(!game.gameOver){
requestAnimationFrame(animate);
}
}
})
UI类
export class UI{
constructor(game){
this.game = game;
this.fontSize = 28
this.fontFamily = 'Creepster'
this.liveImage = document.getElementById('lives')
}
draw(context){
context.save();
context.font = this.fontSize+'px ' + this.fontFamily;
context.textAlign = 'left'
context.fillStyle = this.game.fontColor
context.fillText('Score: '+this.game.score,20,50)
// 时间绘制
context.font=this.fontSize*0.8+'px '+this.fontFamily;
context.fillText('Time: '+(this.game.time*0.001).toFixed(1),20,80);
// 生命值绘制
for(let i=0;i<this.game.lives;i++){
context.drawImage(this.liveImage,25*i+20,95,25,25);
}
// 游戏结束看板
if(this.game.gameOver){
context.textAlign='center';
context.font=this.fontSize + 'px '+this.fontFamily;
if(this.game.score > this.game.winningScore){
context.fillText('you win this game,your score is :'+this.game.score,this.game.width*0.5,this.game.height*0.5);
}
else{
context.fillText('not bad,your score is :'+this.game.score,this.game.width*0.5,this.game.height*0.5);
}
}
context.restore();
}
}
得分效果 floatingMessage.js
export class FloatingMessage{
constructor(value,x,y,targetX,targetY){
this.value = value;
this.x = x;
this.y = y;
this.targetX = targetX;
this.targetY = targetY;
this.markedForDeletion = false;
this.timer = 0;
}
update(){
this.x += (this.targetX-this.x)*0.03;
this.y += (this.targetY-this.y)*0.03;
this.timer++;
if(this.timer > 100){
this.markedForDeletion = true;
}
}
draw(context){
context.font='20px Creepster';
context.fillStyle='white';
context.fillText(this.value,this.x,this.y);
context.fillStyle='black';
context.fillText(this.value,this.x-2,this.y-2);
}
}
接着在player.js中修改
checkCollision(){
this.game.enemies.forEach(enemy => {
if(enemy.x < this.x + this.width &&
enemy.x + enemy.width > this.x &&
enemy.y < this.y + this.height &&
enemy.y+enemy.height > this.y)
{
enemy.markedForDeletion=true
this.game.collisions.push(new CollisionAnimation(this.game,enemy.x+enemy.width*0.5,enemy.y+enemy.height*0.5))
if(this.currentState===this.states[4]||this.currentState===this.states[5]){
this.game.score++;
this.game.floatingMessages.push(new FloatingMessage('+1',enemy.x,enemy.y,150,50));
}
else{
this.setState(6,0);
this.game.score -= 5;
this.game.lives--;
if(this.game.lives<=0){
this.game.gameOver = true;
}
}
}
});
}
附录
[1]源-素材地址
[2]源-视频地址
[3]搬运视频地址(JavaScript 游戏开发)
[4]github-视频的素材以及源码