2、简易版web端荒野乱斗游戏界面

源码地址https://gitee.com/pxlsdz/hyld

演示地址http://121.199.59.80/hyld/

JS模块化

之前的代码将所有的js对象变成全局变量,在日后js对象增加后会有重名导致冲突的风险,所以我们要将它模块化。

改动/home/sdz/project/hyld/game/templates/multiends下的web.html

{% load static %}

<head>
    <link rel="stylesheet" href="https://cdn.acwing.com/static/jquery-ui-dist/jquery-ui.min.css">
    <script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script>
    <link rel="stylesheet" href="{% static 'css/game.css' %}">
</head>

<body style="margin: 0">
    <div id="hyld"></div>
    <script type="module">
        // 模块引入
        import {Hyld} from "{% static 'js/dist/game.js' %}";
        $(document).ready(function(){
            let Hyld = new Hyld("hyld");
        });
    </script>
</body>

并在/home/sdz/project/hyld/game/static/js/src 下的zbase.js的开头加上export即可

注意需要在浏览器设置:F12–> 网络 –> 禁用缓存,保证js最新。

结构

image-20211216020532940

# /home/sdz/project/hyld/game/static/js/src
src
|-- menu
|   `-- zbase.js
|-- playground
|   |-- a
|   |   `-- zbase.js
|   |-- game_map
|   |   `-- zbase.js
|   |-- palyer
|   |   `-- zbase.js
|   |-- particle
|   |   `-- zbase.js
|   |-- skill
|   |   `-- fireball
|   |       `-- zbase.js
|   `-- zbase.js
|-- settings
`-- zbase.js

游戏的模块功能划分

游戏界面完善

  1. 游戏界面HyldPlayground对象中添加html对象hyld-playground
  2. 游戏界面HyldPlayground的作为一个总大类,调用其他类,实现游戏的网页显示。
  3. 需要实现的几个类:
    • 父类游戏引擎:因为涉及到每帧进行渲染,定义一个简易的游戏引擎,之后的类都是它的子类
    • 地图类——游戏画布,随后玩家,火球,敌人都在这张画布上显示
    • 玩家类——实现移动、攻击之类的逻辑
    • 技能类——火球等
    • 粒子效果类——玩家被火球击中后粒子四溅的效果

/home/sdz/project/hyld/game/static/js/src/playground/zbase.js

class HyldPlayground {
    constructor(root) {
        this.root = root;
        this.$playground = $(`<div>游戏界面</div>`);
        this.$playground = $(`<div class="hyld-playground"></div>`);
        // this.hide();

        this.root.$hyld.append(this.$playground);
        this.width = this.$playground.width();
        this.height = this.$playground.height();
        this.game_map = new GameMap(this);
        this.players = []; // 存一下当前的游戏玩家
        this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true));

        for (let i = 0; i < 5; i ++ ) { // 初始化5个AI玩家
            this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, this.get_random_color(), this.height * 0.15, false));
        }

        this.start();
    }

    get_random_color() { // 随机颜色函数
        let colors = ["blue", "red", "pink", "grey", "green"];
        return colors[Math.floor(Math.random() * 5)];
    }

    start() {
    }
    
    show() {  // 打开playground界面
        this.$playground.show();
    }

    hide() {  // 关闭playground界面
        this.$playground.hide();
    }
}

css需要加入 样式,/home/sdz/project/hyld/game/static/css/game.css

hyld-playground {
    width: 100%;
    height: 100%;
    user-select: none;
}

游戏动画实现

思想

模仿电影的原理,每秒让电脑“画”60张图,就实现了一个60帧的动画。

实现思路

  • 定义一个HyldObject
  • 这个HyldObject是对整个游戏界面的类,它可以在画面上显示一个图形,然后根据我们定义的逻辑每帧去刷新画面。
  • 分别定义四个函数 start——画面初始化、update——每帧去刷新画面、on_destroydestroy——删除这个类实现的图案
  • 定义一个每帧都会执行的函数HYLD_ANIMATION,也就是用来刷新界面的函数
  • 使用requestAnimationFrame实现每帧执行上面那个函数,即定时循环操作,随着电脑分辨率刷新渲染页面。

实现

一个渲染的基类

/home/sdz/project/hyld/game/static/js/src/playground/a/zbase.js

//存放所有对象(物体)的数组
let HYLD_OBJECTS = [];

class HyldObject {
    constructor() {
        //每创建一个对象都把它加进数组里
        HYLD_OBJECTS.push(this);

        this.has_called_start = false;  // 是否执行过start函数
        this.timedelta = 0;  // 当前帧距离上一帧的时间间隔
    }

    start() {  // 只会在第一帧执行一次,对当前界面进行一次初始化——玩家的属性值,颜色等等
    }

    update() {  // 每一帧均会执行一次,玩家移动,或者发动技能,每一帧都要进行更新
    }

    on_destroy() {  // 在被销毁前执行一次,当有玩家被杀掉后,有的玩家要加分等等一系列的操作
    }

    destroy() {  // 删掉某个已经死掉的玩家
        this.on_destroy();
        //遍历一遍所有对象,找到当前对象并删除
        for (let i = 0; i < HYLD_OBJECTS.length; i ++ ) {
            if (HYLD_OBJECTS[i] === this) {
                HYLD_OBJECTS.splice(i, 1);
                break;
            }
        }
    }
}

let last_timestamp;
//用递归的结构,保证每一帧都调用一次函数,即一直无限渲染
let HYLD_ANIMATION = function(timestamp) {
    //每一帧要遍历所有物体,让每个物体执行update函数
    for (let i = 0; i < HYLD_OBJECTS.length; i ++ ) {
        let obj = HYLD_OBJECTS[i];
        //用has_called_start标记每个物体,保证每一帧,每个物体只执行一次函数
        if (!obj.has_called_start) {
            obj.start();
            obj.has_called_start = true;
        } else {
            //算出2次调用的间隔时间,为计算速度做准备
            obj.timedelta = timestamp - last_timestamp;
            obj.update();
        }
    }
    last_timestamp = timestamp;

    requestAnimationFrame(HYLD_ANIMATION);
}
requestAnimationFrame(HYLD_ANIMATION);

游戏地图的实现

一块填充整块屏幕的黑色画布,用canvas实现,继承渲染的基类

实现

/home/sdz/project/hyld/game/static/js/src/playground/game_map/zbase.js

class GameMap extends HyldObject { // 继承游戏引擎,这个类是游戏地图,涉及到游戏地图的颜色,形状,等等
    constructor(playground) {
        super();
        this.playground = playground;
        this.$canvas = $(`<canvas></canvas>`);// canvas是一个图形绘制的标签,只是图形容器,需要使用脚本来绘制图形。
        this.ctx = this.$canvas[0].getContext('2d');
        this.ctx.canvas.width = this.playground.width;
        this.ctx.canvas.height = this.playground.height;
        this.playground.$playground.append(this.$canvas);
    }

    start() {
    }

    update() {
        this.render();
    }

    render() {// 渲染函数
        this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)"; // 填充颜色,0.2是透明度,产生一个渐变的过程
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);// 画一个长方形
    }
}

游戏角色的实现

主要

  1. 玩家移动实现

    真人玩家:通过监听鼠标点击,获取鼠标点击的坐标。通过计算原位置到鼠标点击位置的速度,使每一帧的圆刷新在不同的位置,从而实现鼠标点击操控角色移动
    AI玩家:随机游走即可,随机坐标移动即可

    • 改变圆的坐标x,y来实现移动,每一次的移动距离为两帧之间的间隔时间乘圆移动的平均速度:\(timedelta * speeed\).
    • move_to函数计算出x,y坐标移动移动方向:vx,vy,用到了三角函数的知识。
    • 移动的平均速度是在建立该对象时传入的,值为地图宽度的%5每秒
    • 注意每一次移动距离不能超过目标点的位置
  2. 技能实现

    本质也是小球,所以跟玩家实现油异曲同工之妙。由玩家Playershoot_fireball函数创建FireBall即可。

  3. 碰撞检测:断两个圆是否相交,即两圆心的直线距离等于两圆心的半径和

  4. 击中效果

    • 方向修改
    • 速度减慢
    • 面积减小
    • 粒子释放——先实现粒子类——多个粒子向随机不同的方向四散
  5. 监听函数:接收鼠标,键盘。

/home/sdz/project/hyld/game/static/js/src/playground/particle/zbase.js

class Player extends HyldObject {
    /**
     *
     * @param playground 该玩家在哪个地图上
     * @param x 玩家的位置坐标,将来还可能有3d的z轴和朝向坐标
     * @param y
     * @param radius 圆的半径,每个玩家用圆表示
     * @param color 圆的颜色
     * @param speed 玩家的移动速度,用每秒移动高度的百分比表示,因为每个浏览器的像素表示不一样
     * @param is_me 判断当前角色是自己还是敌人
     */
    constructor(playground, x, y, radius, color, speed, is_me) {
        // speed 我们用高度的百分比来表示移动的速度
        //is_me 因为自己和敌人的操作方式是不一样的,需要加一个标签来判断一下是否是自己
        super();
        this.playground = playground;
        this.ctx = this.playground.game_map.ctx; // 画布的一个引用
        this.x = x;
        this.y = y;
        this.vx = 0; // 移动速度
        this.vy = 0; // 移动速度
        this.damage_x = 0; // 收到的攻击的位置
        this.damage_y = 0;
        this.damage_speed = 0; //攻速
        this.move_length = 0; //玩家移动的距离
        this.radius = radius;
        this.color = color;
        this.speed = speed;
        this.is_me = is_me;
        this.eps = 0.1; // 一个浮点运算的标记,小于eps就算是零
        this.friction = 0.9; // 摩擦力
        this.spent_time = 0; // 保护期

        this.cur_skill = null; // 标记当前是否选中技能(未来会有多个技能)
    }

    start() {
        if (this.is_me) {
            this.add_listening_events();
        } else {
            let tx = Math.random() * this.playground.width;
            let ty = Math.random() * this.playground.height;
            this.move_to(tx, ty);
        }
    }
	// 监听函数
    add_listening_events() {
        let outer = this;
        this.playground.game_map.$canvas.on("contextmenu", function() {// 关闭右键菜单
            return false;
        });
        this.playground.game_map.$canvas.mousedown(function(e) {// 将鼠标右键和该函数绑定
            if (e.which === 3) {// 右键是3,滚轮2,左键是1
                outer.move_to(e.clientX, e.clientY);
            } else if (e.which === 1) {
                if (outer.cur_skill === "fireball") {
                    outer.shoot_fireball(e.clientX, e.clientY);
                }

                outer.cur_skill = null; // 释放技能后清空
            }
        });

        $(window).keydown(function(e) {// 监听键盘按键
            if (e.which === 81) {  // 查询keycode可以查到每个键盘按键的值
                outer.cur_skill = "fireball";
                return false;
            }
        });
    }

	//在定义了技能后添加的攻击函数
    shoot_fireball(tx, ty) {
        let x = this.x, y = this.y;
        let radius = this.playground.height * 0.01;
        let angle = Math.atan2(ty - this.y, tx - this.x);
        let vx = Math.cos(angle), vy = Math.sin(angle);
        let color = "orange";
        let speed = this.playground.height * 0.5;
        let move_length = this.playground.height * 1;
        new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, this.playground.height * 0.01);
    }

    get_dist(x1, y1, x2, y2) { //求距离
        let dx = x1 - x2;
        let dy = y1 - y2;
        return Math.sqrt(dx * dx + dy * dy);
    }

    move_to(tx, ty) { // 移动到tx,ty
        this.move_length = this.get_dist(this.x, this.y, tx, ty);
        let angle = Math.atan2(ty - this.y, tx - this.x);
        this.vx = Math.cos(angle); // x y轴上的速度
        this.vy = Math.sin(angle);
    }

    is_attacked(angle, damage) {
        //释放粒子
        for (let i = 0; i < 20 + Math.random() * 10; i ++ ) {
            let x = this.x, y = this.y;
            let radius = this.radius * Math.random() * 0.1;
            let angle = Math.PI * 2 * Math.random();
            let vx = Math.cos(angle), vy = Math.sin(angle);
            let color = this.color;
            let speed = this.speed * 10;
            let move_length = this.radius * Math.random() * 5;
            new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length);
        }
        this.radius -= damage;
        if (this.radius < 10) {
            this.destroy();
            return false;
        }
        this.damage_x = Math.cos(angle);
        this.damage_y = Math.sin(angle);
        this.damage_speed = damage * 100;
        this.speed *= 0.8;
    }

    update() {
        this.spent_time += this.timedelta / 1000; // 保护期累加
        if (!this.is_me && this.spent_time > 4 && Math.random() < 1 / 300.0) {
            // 随机选中一名玩家,攻击
            let player = this.playground.players[Math.floor(Math.random() * this.playground.players.length)];
            // 对其行动轨迹预判
            let tx = player.x + player.speed * this.vx * this.timedelta / 1000 * 0.3;
            let ty = player.y + player.speed * this.vy * this.timedelta / 1000 * 0.3;
            this.shoot_fireball(tx, ty);
        }

        if (this.damage_speed > 10) {
            this.vx = this.vy = 0;
            this.move_length = 0;
            this.x += this.damage_x * this.damage_speed * this.timedelta / 1000;
            this.y += this.damage_y * this.damage_speed * this.timedelta / 1000;
            this.damage_speed *= this.friction;
        } else {
            if (this.move_length < this.eps) {
                this.move_length = 0;
                this.vx = this.vy = 0;
                if (!this.is_me) {
                    let tx = Math.random() * this.playground.width;
                    let ty = Math.random() * this.playground.height;
                    this.move_to(tx, ty);
                }
            } else {
                // 两帧间的移动距离
                let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000);
                // 这个意思是,我们要形成一个移动的动画,所以每帧都移动一点点,this.speed * this.timedelta / 1000就是每次移动的一点点
                this.x += this.vx * moved;
                this.y += this.vy * moved;
                this.move_length -= moved;
            }
        }
        this.render();
    }

    render() { // 渲染函数,画一个圆
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        this.ctx.fillStyle = this.color;
        this.ctx.fill();
    }

    on_destroy() {
        for (let i = 0; i < this.playground.players.length; i ++ ) {
            if (this.playground.players[i] === this) {
                this.playground.players.splice(i, 1);
            }
        }
    }
}

游戏技能的实现

继承基类实现火球的基本逻辑

技能的属性一般要包括:技能的释放范围,技能的冷却时间,技能的伤害,技能的释放方向,技能的弹道速度。

技能释放的按键操作方式:利用which 事件属性,和对应的Keycode设置特定的操作方式实现思路。

碰撞检测,当发生碰撞就发动攻击——调用player类的is_attached函数。

/home/sdz/project/hyld/game/static/js/src/playground/skill/fireball/zbase.js

class FireBall extends HyldObject {
    constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
        // player 是谁发射的火球
        // damage 伤害值 
        super();
        this.playground = playground;
        this.player = player;
        this.ctx = this.playground.game_map.ctx;
        this.x = x;
        this.y = y;
        this.vx = vx;
        this.vy = vy;
        this.radius = radius;
        this.color = color;
        this.speed = speed;
        this.move_length = move_length;
        this.damage = damage;
        this.eps = 0.1;
    }

    start() {
    }

    update() {
        if (this.move_length < this.eps) { // 火球消失了
            this.destroy();
            return false;
        }

        let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000);
        this.x += this.vx * moved;
        this.y += this.vy * moved;
        this.move_length -= moved;

        for (let i = 0; i < this.playground.players.length; i ++ ) {
            let player = this.playground.players[i];
            if (this.player !== player && this.is_collision(player)) {
                this.attack(player);
            }
        }

        this.render();
    }

    get_dist(x1, y1, x2, y2) {
        let dx = x1 - x2;
        let dy = y1 - y2;
        return Math.sqrt(dx * dx + dy * dy);
    }

    is_collision(player) {
        let distance = this.get_dist(this.x, this.y, player.x, player.y);
        if (distance < this.radius + player.radius)
            return true;
        return false;
    }

    attack(player) {
        let angle = Math.atan2(player.y - this.y, player.x - this.x);
        player.is_attacked(angle, this.damage);
        this.destroy();
    }

    render() {
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        this.ctx.fillStyle = this.color;
        this.ctx.fill();
    }
}

粒子效果的实现

粒子也如同玩家一样,被看作地图上的一个对象,每个粒子从玩家上产生,会随机移动,逐渐消失

/home/sdz/project/hyld/game/static/js/src/playground/particle/zbase.js中的Particle

class Particle extends HyldObject {
    constructor(playground, x, y, radius, vx, vy, color, speed, move_length) {
        // speed 我们用高度的百分比来表示移动的速度
        super();
        this.playground = playground;
        this.ctx = this.playground.game_map.ctx;
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.vx = vx;
        this.vy = vy;
        this.color = color;
        this.speed = speed;
        this.move_length = move_length;
        this.friction = 0.9;
        this.eps = 1;
    }

    start() {
    }

    update() {
        if (this.move_length < this.eps || this.speed < this.eps) {
            this.destroy();
            return false;
        }

        let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000);
        this.x += this.vx * moved;
        this.y += this.vy * moved;
        this.speed *= this.friction;
        this.move_length -= moved; // 要移动的距离记得更新
        this.render();
    }

    render() {
        this.ctx.beginPath(); // 画一个圆
        this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        this.ctx.fillStyle = this.color;
        this.ctx.fill();
    }
}

这个对象在/home/sdz/project/hyld/game/static/js/src/playground/player/zbase.js中的is_attacked()中被创建 。

火球相互抵消的实现

思路

1.该游戏中的所有对象都放在HYLD_OBJECTS = []

2.每个platground对象中都有playersfireball等数组,储存了该地图下的各种对象

3.火球相互抵消这个操作就是在每个对象所在platground中的fireball数组下进行操作从而实现的

4.删除一个对象,如火球。在HYLD_OBJECTS = []中删除时,要调用对象的destroy()函数。

在其他数组,如platground对象中的fireball数组中删除时,删除过程要写在on_destroy()函数中。

实现

    /**
     * 从playground.fireballs中将火球删除
     */
    on_destroy() {
        for (let i = 0; i < this.playground.fireballs.length; i++) {
            if (this.playground.fireballs[i] === this) {
                this.playground.fireballs.splice(i, 1);
            }
        }
    }
    update() {
        if (this.move_length < this.eps) {
            this.destroy();
            return false;
        }
        //每一帧都刷新火球的位置
        let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000);
        this.x += this.vx * moved;
        this.y += this.vy * moved;
        this.move_length -= moved;

        //实现火球碰撞后相互抵消,将火球从HYLD_OBJECTS = [],中删除
        for (let i = 0; i < this.playground.fireballs.length; i++) {
            let fireball = this.playground.fireballs[i];

            if (fireball != this && this.is_collision(fireball)) {
                this.destroy();
                fireball.destroy();
                break;
            }
        }

        this.render();
    }

posted @ 2021-12-22 19:18  pxlsdz  阅读(111)  评论(0编辑  收藏  举报