Web学习(五)中期项目-简易拳皇

Web学习(五)中期项目-简易拳皇

项目参考地址:https://git.acwing.com/yxc/kof

游戏的基本原理:主要依靠requestAnimationFrame实现,该函数会在下次浏览器刷新页面之前执行一次,通常会用递归写法使其每秒执行60次func函数,每秒画60次物体,通过坐标改变物体位置,人眼看到的结果就是物体在移动。

1.创建表示游戏窗口的类

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">
    <link rel="stylesheet" href="/static/css/base.css">
    <script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script>
    <title>拳皇</title>
</head>

<body>
    <div id="kof"></div>

    <script type="module">
        import { KOF } from '/static/js/base.js'

        let kof = new KOF('kof');
    </script>
</body>

</html>

css:

#kof {
    width: 1280px;
    height: 720px;

    background-image: url('/static/images/background/0.gif');
    background-size: 200% 100%;
    background-position: top;
    position: absolute;
}

base.js:

KOF窗口大类包含两个小类:玩家与地图

// 创建一个类KOF表示当前的大窗口
class KOF {
    constructor(id) {  //构造函数,需要传入id
        this.$kof = $('#' + id);//jQuery选择器

    }
}

export {
    KOF
}

2.创建ac_game_object类

game_map,player等对象都需要在每秒画60次以达到移动效果,所以我们创建一个共同的基类ac_game_object, game_map,player等对象继承ac_game_object基类即可。

let AC_GAME_OBJECTS = []

class AcGameObject {
    constructor() {
        AC_GAME_OBJECTS.push(this); //存储对象

        this.timedelta = 0; //时间间隔,每个object对象都需要存储当前一帧与上一帧的时间间隔,每个物体的速度取决于时间间隔
        this.has_call_start = false; //表示当前对象是否调用过start函数

    }

    start() { //初始执行一次

    }

    updats() { //每一帧执行一次(第一帧除外)

    }

    destroy() { //删除对象
        for (let i in AC_GAME_OBJECTS) {    //for in 是枚举下标,for of是枚举值
            if (AC_GAME_OBJECTS[i] === this) {
                AC_GAME_OBJECTS.splice(i, 1);
                break;
            }
        }
    }
}

let last_timestamp; //上一帧执行时刻

let AC_GAME_OBJECTS_FRAME = (timestamp) => {  //timestamp它表示requestAnimationFrame() 开始去执行回调函数的时刻,时间戳
    for (let obj of AC_GAME_OBJECTS) {
        if (!obj.has_call_start) {  //如果对象没有执行过start函数
            obj.start();
            obj.has_call_start = true;
        } else {
            obj.timedelta = timestamp - last_timestamp; //时间间隔,当前时刻-上一帧时刻
            obj.update();
        }
    }

    last_timestamp = timestamp;  //更新上次调用函数的时刻
    requestAnimationFrame(AC_GAME_OBJECTS_FRAME); //递归调用
}

requestAnimationFrame(AC_GAME_OBJECTS_FRAME);

export {
    AcGameObject
}

3.定义game_map对象

import { AcGameObject } from '/static/js/ac_game_object/base.js';

export class GameMap extends AcGameObject { //继承
    constructor(root) {
        super();

        this.root = root;
        this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); //jQuery用法,tabindex=0使得可以聚焦读取键盘输入
        this.ctx = this.$canvas[0].getContext('2d'); //取出canvas,这里参考canvas用法
        this.root.$kof.append(this.$canvas); //将canvas加入id为kof的div里
        this.$canvas.focus(); //使得convas可以聚焦

    }

    start() {    //开始时执行

    }

    update() {   //每一帧执行
        this.render();
    }

    render() {   //渲染函数中需要将每一帧地图都清空,否则物体运动过程会一直停留在地图上
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

4.定义player对象

import { AcGameObject } from "/static/js/ac_game_object/base.js";

export class Player extends AcGameObject {
    constructor(root, info) { //将初始参数放至构造函数中,这里root表示大类KOF,大类KOF包含两个小类玩家与地图
        super();

        this.root = root;
        this.id = info.id;
        this.x = info.x;
        this.y = info.y;
        this.width = info.width;
        this.height = info.height;
        this.color = info.color;

        this.direction = 1;

        this.vx = 0;
        this.vy = 0;
        this.speedx = 400;  // 水平速度
        this.speedy = -1000;  // 跳起的初始速度
        this.gravity = 50;

        this.ctx = this.root.game_map.ctx; //这里引入canvas便于下一步操作
    }

    start() {

    }

    update() {
        this.render();
    }

    render() {

        //测试代码
        this.ctx.fillstyle = this.color;
        this.ctx.fillRect(this.x, this.y, this.width, this.height);
    }

}

在主js中创建地图与玩家对象

import { GameMap } from '/static/js/game_map/base.js';
import { Player } from '/static/js/player/base.js';


// 创建一个类KOF表示当前的大窗口,此大类包含玩家对象,地图对象
class KOF {
    constructor(id) {  //构造函数,需要传入id
        this.$kof = $('#' + id);//jQuery选择器

        //创建地图
        this.game_map = new GameMap(this);

        //创建玩家
        this.players = [  //player对象需要传入root与info
            new Player(this, {
                id: 0,
                x: 200,
                y: 0,
                width: 120,
                height: 200,
                color: 'blue',
            }),
            new Player(this, {
                id: 1,
                x: 900,
                y: 0,
                width: 120,
                height: 200,
                color: 'red',
            }),
        ];

    }
}

export {
    KOF
}

5.定义controller对象

controller对象用于读取键盘输入

export class Controller {
    constructor($canvas) {
        this.$canvas = $canvas;

        this.pressed_keys = new Set(); //set用于存储当前按住了哪个键
        this.start();
    }

    start() {
        let outer = this; //这里pressed_keys是Controller对象下的值,不是convas的,所以要使用outer
        this.$canvas.keydown(function (e) { //按下
            outer.pressed_keys.add(e.key); //这里this指的是canvas,outer指的是controller
        });

        this.$canvas.keyup(function (e) {  //放开
            outer.pressed_keys.delete(e.key);
        });
    }
}

6.在player对象中添加update_control函数

update_control() { //更新按下的键位
        let w, a, d, space;
        if (this.id === 0) { // 玩家1
            w = this.pressed_keys.has('w');
            a = this.pressed_keys.has('a');
            d = this.pressed_keys.has('d');
            space = this.pressed_keys.has(' ');
        } else {   //玩家2
            w = this.pressed_keys.has('ArrowUp');
            a = this.pressed_keys.has('ArrowLeft');
            d = this.pressed_keys.has('ArrowRight');
            space = this.pressed_keys.has('Enter');
        }

        if (this.status === 0 || this.status === 1) { //静止状态或移动状态
            if (space) {
                this.status = 4;
                this.vx = 0;
                this.frame_current_cnt = 0;
            } else if (w) { //跳跃
                if (d) {
                    this.vx = this.speedx;
                } else if (a) {
                    this.vx = -this.speedx;
                } else {
                    this.vx = 0;
                }
                this.vy = this.speedy;
                this.status = 3;
                this.frame_current_cnt = 0;
            } else if (d) { //向右
                this.vx = this.speedx;
                this.status = 1;
            } else if (a) { //向左
                this.vx = -this.speedx;
                this.status = 1;
            } else {
                this.vx = 0;
                this.status = 0;
            }
        }
    }

7.添加动画(人物图片)

参考:https://stackoverflow.com/questions/48234696/how-to-put-a-gif-with-canvas
新建utils文件夹中存放参考的gif.js
player中新建kyo.js:

import { Player } from '/static/js/player/base.js';
import { GIF } from '/static/js/utils/gif.js';

export class Kyo extends Player {
    constructor(root, info) {
        super(root, info);

        this.init_animations();
    }

    init_animations() {
        let outer = this;
        let offsets = [0, -22, -22, -140, 0, 0, 0];  //偏移量,因为图片高度的原因,对应7种不同状态时回到同一水平线需要的偏移量

        for (let i = 0; i < 7; i++) { //对图片循环处理
            let gif = GIF();
            gif.load(`/static/images/player/kyo/${i}.gif`);
            this.animations.set(i, {  //这里的animations是在player中定义过的Map用来存储键值对,set是插入键值对
                gif: gif,
                frame_cnt: 0,  // 总图片数
                frame_rate: 5,  // 每5帧过度一次
                offset_y: offsets[i],  // y方向偏移量
                loaded: false,  // 是否加载完整
                scale: 2,  // 放大多少倍
            });

            gif.onload = function () {
                let obj = outer.animations.get(i); //Map用法get是查找关键字
                obj.frame_cnt = gif.frames.length;
                obj.loaded = true; //表示加载完整

                if (i === 3) {  //如果是第3种图片即跳跃状态,更改帧率
                    obj.frame_rate = 4;
                }
            }
        }
    }
}

在player中的base,js修改render渲染函数:

render() {  //渲染函数

        // //测试代码,画出矩形
        // this.ctx.fillstyle = this.color;
        // this.ctx.fillRect(this.x, this.y, this.width, this.height);

        let status = this.status; //取出当前状态

        //如果和速度反方向,状态变为2(后退)
        if (this.status === 1 && this.direction * this.vx < 0) status = 2;

        let obj = this.animations.get(status);  //将当前状态status传入animations中

        if (obj && obj.loaded) {
            if (this.direction > 0) { //如果是正向
                //frame_current_cnt表示当前记录了多少帧
                //frame_current_cnt % obj.frame_cnt 当前帧数 % 图片总帧数,因为图片需要循环播放
                //frame_rate控制图片播放速率
                let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;

                //取出第k张图片
                let image = obj.gif.frames[k].image;
                //画出图像,  this.y + obj.offset_y:纵方向加上偏移量,  image.width * obj.scale:图片大小乘以缩放倍数
                this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);
            } else {
                this.ctx.save();
                this.ctx.scale(-1, 1); // x坐标乘-1,y坐标不变,即水平反转
                this.ctx.translate(-this.root.game_map.$canvas.width(), 0);

                let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;
                let image = obj.gif.frames[k].image;
                //水平翻转之后,两图像初始位置将会一致,所以初始位置需要对称过去
                this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);

                this.ctx.restore(); //恢复坐标系
            }
        }

        //状态为攻击,
        if (status === 4 || status === 5 || status === 6) {
            if (this.frame_current_cnt == obj.frame_rate * (obj.frame_cnt - 1)) {
                if (status === 6) {
                    this.frame_current_cnt--;
                } else {
                    this.status = 0; // 回归静止状态
                }
            }
        }

        this.frame_current_cnt++; //下一帧

    }

8.让俩位玩家位置对称

update_direction() {  //更新位置(面朝方向)
        if (this.status === 6) return;

        let players = this.root.players;
        if (players[0] && players[1]) {
            let me = this, you = players[1 - this.id];
            if (me.x < you.x) me.direction = 1;  //如果我在左,则我面朝的方向为正方向
            else me.direction = -1;
        }
    }

9.攻击与被攻击实现

攻击函数:

update_attack() {  //更新攻击函数
        if (this.status === 4 && this.frame_current_cnt === 18) { //处于攻击状态,且在18帧时(18帧时拳头挥出去)
            let me = this, you = this.root.players[1 - this.id];
            let r1;

            //r1出拳挥出的长方形,只要此长方形与人物模型长方形发生碰撞则认为击中了
            if (this.direction > 0) {      //正方向
                r1 = {
                    x1: me.x + 120,
                    y1: me.y + 40,
                    x2: me.x + 120 + 100,
                    y2: me.y + 40 + 20,
                };
            } else {    //反方向 
                r1 = {
                    x1: me.x + me.width - 120 - 100,
                    y1: me.y + 40,
                    x2: me.x + me.width - 120 - 100 + 100,
                    y2: me.y + 40 + 20,
                };
            }

            //r2为人物模型的长方形
            let r2 = {
                x1: you.x,
                y1: you.y,
                x2: you.x + you.width,
                y2: you.y + you.height
            };

            if (this.is_collision(r1, r2)) {  //如果碰撞检测成立
                you.is_attack(); //调用被攻击函数
            }
        }
    }

碰撞检测函数:

is_collision(r1, r2) { //碰撞检测
        if (Math.max(r1.x1, r2.x1) > Math.min(r1.x2, r2.x2))
            return false;
        if (Math.max(r1.y1, r2.y1) > Math.min(r1.y2, r2.y2))
            return false;
        return true;
    }

被攻击函数:

is_attack() {   //被攻击函数
        if (this.status === 6) return;

        this.status = 5;
        this.frame_current_cnt = 0;
    }

10.增加血条

更改css

#kof {
    width: 1280px;
    height: 720px;

    background-image: url('/static/images/background/0.gif');
    background-size: 200% 100%;
    background-position: top;
    position: absolute;
}

#kof>.kof-head {
    width: 100%;
    height: 80px;
    position: absolute;
    top: 0;
    display: flex;
    align-items: center;
}

/* 血条0框 */
#kof>.kof-head>.kof-head-hp-0 {
    height: 40px;
    width: calc(50% - 60px);
    margin-left: 20px;
    border: white 5px solid;
    border-right: none;
    box-sizing: border-box;
}

/* 计时器 */
#kof>.kof-head>.kof-head-timer {
    height: 60px;
    width: 80px;
    background-color: orange;
    border: white 5px solid;
    box-sizing: border-box;
    color: white;
    font-size: 30px;
    font-weight: 800;
    text-align: center;
    line-height: 50px;
    user-select: none;
}

/* 血条1框 */
#kof>.kof-head>.kof-head-hp-1 {
    height: 40px;
    width: calc(50% - 60px);
    border: white 5px solid;
    border-left: none;
    box-sizing: border-box;
}

/* 血条0 -掉血*/
#kof>.kof-head>.kof-head-hp-0>div {
    background-color: red;
    height: 100%;
    width: 100%;
    float: right;
}

/* 血条1 -掉血*/
#kof>.kof-head>.kof-head-hp-1>div {
    background-color: red;
    height: 100%;
    width: 100%;
}

/* 血条0 */
#kof>.kof-head>.kof-head-hp-0>div>div {
    background-color: lightgreen;
    height: 100%;
    width: 100%;
    float: right;
}

/* 血条1 */
#kof>.kof-head>.kof-head-hp-1>div>div {
    background-color: lightgreen;
    height: 100%;
    width: 100%;
}

gamemap中base.js地图中添加血条元素:

import { AcGameObject } from '/static/js/ac_game_object/base.js';
import { Controller } from '/static/js/controller/base.js';

export class GameMap extends AcGameObject { //继承
    constructor(root) {
        super();

        this.root = root;
        this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); //jQuery用法,tabindex=0使得可以聚焦读取键盘输入
        this.ctx = this.$canvas[0].getContext('2d'); //取出canvas,这里参考canvas用法
        this.root.$kof.append(this.$canvas); //将canvas加入id为kof的div里
        this.$canvas.focus(); //使得convas可以聚焦

        this.controller = new Controller(this.$canvas); //将controller加入地图中,用于读取键盘输入

        //增加地图上血条div
        //地图上增加计时器div
        //增加地图上血条div
        this.root.$kof.append($(`<div class="kof-head">
        <div class="kof-head-hp-0"><div><div></div></div></div>  
        <div class="kof-head-timer">60</div>                     
        <div class="kof-head-hp-1"><div><div></div></div></div>  
    </div>`));

    }

    start() {    //开始时执行

    }

    update() {   //每一帧执行
        this.render();
    }

    render() {   //渲染函数中需要将每一帧地图都清空,否则物体运动过程会一直停留在地图上
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

player文件夹下base.js增加被攻击函数,实现扣血逻辑:

is_attack() {   //被攻击函数
        if (this.status === 6) return;

        this.status = 5;
        this.frame_current_cnt = 0;

        this.hp = Math.max(this.hp - 20, 0);

        this.$hp_div.animate({  //jquery中.animate可以实现渐变效果
            width: this.$hp.parent().width() * this.hp / 100  //通过更改血条宽度实现扣血的显示
        }, 300);   //里面的div变化慢一些,实现掉血效果
        this.$hp.animate({
            width: this.$hp.parent().width() * this.hp / 100
        }, 600);   //外面的div变化快一些

        if (this.hp <= 0) {  //死亡
            this.status = 6;
            this.frame_current_cnt = 0;
            this.vx = 0;
        }
    }

11.增加计时器

在gamemap文件夹base.js中实现计时器逻辑

import { AcGameObject } from '/static/js/ac_game_object/base.js';
import { Controller } from '/static/js/controller/base.js';

export class GameMap extends AcGameObject { //继承
    constructor(root) {
        super();

        this.root = root;
        this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); //jQuery用法,tabindex=0使得可以聚焦读取键盘输入
        this.ctx = this.$canvas[0].getContext('2d'); //取出canvas,这里参考canvas用法
        this.root.$kof.append(this.$canvas); //将canvas加入id为kof的div里
        this.$canvas.focus(); //使得convas可以聚焦

        this.controller = new Controller(this.$canvas); //将controller加入地图中,用于读取键盘输入

        //增加地图上血条div
        //地图上增加计时器div
        //增加地图上血条div
        this.root.$kof.append($(`<div class="kof-head">
        <div class="kof-head-hp-0"><div><div></div></div></div>  
        <div class="kof-head-timer">60</div>                     
        <div class="kof-head-hp-1"><div><div></div></div></div>  
    </div>`));

        this.time_left = 60000;  // 单位:毫秒
        this.$timer = this.root.$kof.find(".kof-head-timer");

    }

    start() {    //开始时执行

    }

    update() {   //每一帧执行
        this.time_left -= this.timedelta; //时间逐渐减少
        if (this.time_left < 0) {  //时间结束后两人都死亡
            this.time_left = 0;

            let [a, b] = this.root.players;
            if (a.status !== 6 && b.status !== 6) {
                a.status = b.status = 6;
                a.frame_current_cnt = b.frame_current_cnt = 0;
                a.vx = b.vx = 0;
            }
        }

        this.$timer.text(parseInt(this.time_left / 1000));  //修改time文本时间

        this.render();
    }

    render() {   //渲染函数中需要将每一帧地图都清空,否则物体运动过程会一直停留在地图上
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rgb2gray

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值