JavaScript游戏开发(4)(笔记)

素材可以去一位大佬放在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-视频的素材以及源码

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值