Django项目笔记(三)——简单游戏的实现(模块拆分化详解)

Django上课笔记(三)——简单游戏的实现(模块拆分化详解)

上完这节课。我只想说一句话y总永远滴神!!

这节课真的好难鸭/(ㄒoㄒ)/~~

为了从y总浩瀚的知识中吸取那么一点点,我会逐步拆分这次的代码,let’s go!!

上次课的补充

改动~/acapp/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="ac_game_12345678"></div>
    <script type="module">
//-----------------------------------------------------------------------------------
        import {AcGame} from "{% static 'js/dist/game.js' %}";
//--------------------------------------------------------------------------------------
        $(document).ready(function(){
            let ac_game = new AcGame("ac_game_12345678");
        });
    </script>
</body>


并在~/acapp/game/static/js/src 下的zbase.js的开头加上export

准备工作

为了调试方便(刷新后直接看到首页),将菜单界面关闭,只显示游戏界面

注释掉~/acapp/game/static/js/src 下的zbase.js中的this.menu = new AcGameMenu(this);

游戏的模块功能划分

想要把整个代码拆分成模块,需要关注以下几个问题

思维导图

下面来逐一讨论以下这些问题

游戏动画的实现

思想

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

语法知识补充

1.这里start()update()on_destroy() 应该是参考了各大框架中生命周期的设计思想,可以参考Vue 实例 — Vue.js (vuejs.org)

2.数组的splice()函数,参考JavaScript splice() 方法

3.requestAnimationFrame()函数的优势,参考requestAnimationFrame详解

实现

一个渲染的基类

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

class AcGameObject {
    constructor() {
        //每创建一个对象都把它加进数组里
        AC_GAME_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 < AC_GAME_OBJECTS.length; i ++ ) {
            if (AC_GAME_OBJECTS[i] === this) {
                AC_GAME_OBJECTS.splice(i, 1);
                break;
            }
        }
    }
}

let last_timestamp;
//用递归的结构,保证每一帧都调用一次函数,即一直无限渲染
let AC_GAME_ANIMATION = function(timestamp) {
    //每一帧要遍历所有物体,让每个物体执行update函数
    for (let i = 0; i < AC_GAME_OBJECTS.length; i ++ ) {
        let obj = AC_GAME_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(AC_GAME_ANIMATION);
}


requestAnimationFrame(AC_GAME_ANIMATION);

destroy()函数: 在AC_GAME_OBJECTS = []删除该对象。

on_destroy():在其他数组中删除该对象。

destroy()函数的开头调用了on_destroy()函数。所以要在其他数组中删除删除对象,只需要实现on_destroy()函数

游戏地图的实现

语法补充:

1.canvas标签:HTML5 Canvas | 菜鸟教程 (runoob.com)
HTML 画布 | 菜鸟教程 (runoob.com)

用canvas实现,继承渲染的基类

实现

game/static/js/src/playground/game_map/zbase.js

class GameMap extends AcGameObject {
    constructor(playground) {
        //super()等价于AcGameObject.prototype.constructor.call(this)
        super();
        this.playground = playground;
        this.$canvas = $(`<canvas></canvas>`);
        this.ctx = this.$canvas[0].getContext('2d');
        this.ctx.canvas.width = this.playground.width;
        this.ctx.canvas.height = this.playground.height;
        //这里的playground在定义时时任意值,但在调用时如果传入AcGamePlayground类就指这个类
        this.playground.$playground.append(this.$canvas);
    }

    start() {
    }

    update() {
        this.render();
    }

    render() {
        //改变背景的不透明度,以实现移动残影
        this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
        //fillRect()绘制矩形的方法
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

移动残影的实现

改变背景的不透明度

游戏角色的实现

思想

建立玩家对象时,最重要的就是考虑角色有哪些属性

语法知识补充

HTML canvas arc() 方法

HTML canvas fill() 方法

实现
class Player extends AcGameObject{
    /**
     *
     * @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) {
        super();
        this.playground = playground;
        this.x = x;
        this.y = y;
        this.color = color;
        this.speed = speed;
        this.is_me = is_me;
        //玩家所处地图的画布
        this.ctx = this.playground.game_map.ctx;
        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;
        //表示精度,误差在eps内就算0
        this.eps = 0.1;
        this.friction = 0.9;
        this.spent_time = 0;

    }
    start(){

    }
    update(){
        //每一帧都要渲染圆
        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();
        //--------------------------------------------------------------
    }
}

游戏角色移动的实现

思路

通过监听鼠标点击,获取鼠标点击的坐标。通过计算原位置到鼠标点击位置的速度,使每一帧的圆刷新在不同的位置,从而实现鼠标点击操控角色移动

计算每一帧圆所在位置的过程:

1.在基类AcGameObject中计算出了两帧之间的间隔时间timedelta

2.move_to函数计算出移动方向,用到了三角函数的知识

3.移动的平均速度在建立该对象时传入的,值为地图宽度的%5每秒

4.两帧之间的移动距离等于两帧之间的间隔时间timedelta*移动的平均速度

5.新的一帧圆的位置是上一帧的圆位置加上两帧之间的移动距离在x,y方向上的分量,这个位置不能超过目标点的位置

语法知识

JavaScript atan2() 方法

实现
     /**
     * 计算2点间的距离
     * @returns 两点间的直线距离
     */
    get_dist(x1, y1, x2, y2) {
        let dx = x1 - x2;
        let dy = y1 - y2;
        return Math.sqrt(dx * dx + dy * dy);
    }


    /**
     * 计算出移动方向的函数
     * @param tx 目标点的横坐标
     * @param ty 目标点的纵坐标
     */
    move_to(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);
        this.vy = Math.sin(angle);
    }
    /**
     * 鼠标点击的操作
     */
    add_listening_events() {
        let outer = this;
        //禁用鼠标右键点击显示菜单的事件
        this.playground.game_map.$canvas.on("contextmenu", function () {
            return false;
        });
        this.playground.game_map.$canvas.mousedown(function (e) {
            //e.which === 3,点击鼠标右键的事件
            //e.which === 1,点击鼠标左键的事件
            if (e.which === 3) {
                // console.log(e.clientX, e.clientY);
                //每一次点击都要计算出当前点到点击位置的距离
                outer.move_to(e.clientX, e.clientY);

            }
        });

    }

update() {
      if (this.move_length < this.eps) {
          this.move_length = 0;
          this.vx = this.vy = 0;

      } else {
          //计算出两帧间的移动距离
          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;
      }

        //每一帧都要渲染圆
        this.render();
    }

游戏技能系统的实现

思路

建立技能类的思路和建立玩家类的思路差不多

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

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

电脑玩家随机移动的实现

思路

让电脑玩家在第一帧随机一个目标地点,在到达目标地点的帧再随机一个新的目标地点

语法知识

JavaScript random() 方法

实现
/**
 * 每一帧都执行
 */
update()
{

    //玩家已经走到了目标地点
    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.x += this.vx * moved;
        this.y += this.vy * moved;
        this.move_length -= moved;
    }


    //每一帧都要渲染圆
    this.render();
}

碰撞检测的实现

思路

圆的碰撞检测思路很简单,就是判断两个圆是否相交,即两圆心的直线距离等于两圆心的半径和

实现

game/static/js/src/playground/skill/fireball/zbase.js中的is_collision()函数

    /**
     * 实现碰撞检测
     * @param player
     * @returns {boolean}
     */
    is_collision(obj) {
        let dis = this.get_dist(this.x, this.y, obj.x, obj.y);
        return dis < this.radius + obj.radius;
    }

这个函数在game/static/js/src/playground/skill/fireball/zbase.js中的update()函数中被使用

击退效果的实现

思路

计算火球来的方向,以火球的弹道方向作为玩家的被击退方向,玩家体积大小有一定变化,移动速度也要有一定变化

实现

game/static/js/src/playground/player/zbase.js中的is_attacked()实现

    /**
     * 被攻击后的效果
     * @param angle 受到攻击后的角度,用于实现击退效果
     * @param damage 技能的伤害
     */
    is_attacked(angle, damage) {
        //实现被攻击后的粒子效果
        for (let i = 0; i < gameParameters.particle_number[0] + Math.random() * 					gameParameters.particle_number[1]; i++) {
            //这里参考了大佬的代码,比y总的传参更合理
            new Particle(this.playground, this);
        }
        //受到攻击的玩家,移速变慢,体积变小,发射技能的弹道速度变慢
        this.radius -= damage;
        this.speed *= gameParameters.reduce_ratio;
        if (this.radius < gameParameters.dead_szie) {
            this.destroy();
            return false;
        }
        //计算受到攻击后的击退方向
        this.damage_x = Math.cos(angle);
        this.damage_y = Math.sin(angle);
        this.damage_speed = this.radius * 50;


    }

粒子效果的实现

思路

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

实现

game/static/js/src/playground/particle/zbase中的Particle

class Particle extends AcGameObject {
    /**
     * 粒子类
     * @param playground 在哪张地图上
     * @param player 哪个玩家扩散出的粒子
     */
    constructor(playground, player) {
        super();
        this.playground = playground;
        this.player = player;
        //粒子的画布
        this.ctx = this.playground.game_map.ctx;
        //粒子的位置
        this.x = player.x;
        this.y = player.y;
        //粒子的颜色应该和玩家的颜色相等
        this.color = player.color;
        // 粒子半径
        this.radius = Math.random() * player.radius * gameParameters.particle_size_percent;
        // 释放速度
        this.speed = player.speed * gameParameters.particle_speed_percent;

        // 固定参数,粒子的移动距离
        this.move_length = Math.max(gameParameters.particle_move_length[0], Math.random()) * player.radius * gameParameters.particle_move_length[1];
        // 减速摩擦力
        this.friction = gameParameters.particle_friction;
        // 误差范围
        this.eps = 1;

        // 随机参数,释放方向
        // 弧度制
        this.angle = Math.PI * 2 * Math.random();
        //粒子的随机移动方向
        this.vx = Math.cos(this.angle);
        this.vy = Math.sin(this.angle);

    }

    /**
     * 只在第一帧执行
     */
    start() {

    }

    /**
     * 每一帧都执行
     */
    update() {
        if (this.radius < this.eps) {
            this.destroy();
            return false;
        }

        this.radius *= gameParameters.particle_feed;
        //每一帧都刷新粒子的位置
        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;
        this.speed *= this.friction;

        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();
        //--------------------------------------------------------------
    }


}

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

简单移动预测的实现

思路

攻击某玩家时,传入其移动方向,计算某玩家在x秒后的位置,朝该位置发射火球

实现
        if (!this.is_me && Math.random() < gameParameters.AIs_attack_frequency) {
            let player = this.playground.players[Math.floor(Math.random() * this.playground.players.length)];
            //是否开启相互攻击
            if (!gameParameters.attack_eachother) {
                player = this.playground.players[0];
            }
            //实现简单的移动预测
            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);
        }

火球相互抵消的实现

思路

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

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

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

4.删除一个对象,如火球。在AC_GAME_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;

        //实现火球碰撞后相互抵消,将火球从AC_GAME_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();
    }

自己的一些想法

y总上课时候说过游戏的体验最重要的就是参数的设置

既然这样,我们为什么不把所有游戏参数集中到一个文件中呢?

这样既方便我们调试,又给后期,前后端的数据沟通带来了方便

只需要在该文件下改动参数即可生效

配置文件game/static/js/src/config.js

var gameParameters = {

//--------------playground/game_map/zbase.js----------------
    //背景颜色和不透明度
    "background_color": "rgba(0, 0, 0, 0.2)",
//--------------------------------------------------------------


//--------------playground/particle/zbase.js----------------

    //粒子效果的最大粒子半径/玩家半径的比值
    "particle_size_percent": 0.4,

    //粒子速度/其玩家速度,的比值
    "particle_speed_percent": 20,

    //粒子移动距离参数Math.max(0.5, Math.random()) * player.radius * 4
    "particle_move_length": [0.5, 4],

    //减速摩擦力
    "particle_friction": 0.85,

    //粒子每帧的消失比例
    "particle_feed": 0.98,

//----------------------------------------------------------------------

//--------------playground/player/zbase.js----------------

    //电脑玩家自动攻击的频率
    "AIs_attack_frequency": 1/360,

    //最小击退速度
    "damage_speed":10,

    //是否开启互相攻击
    "attack_eachother":false,

    //火球大小/画布高度
    "fireball_size":0.01,

    //火球弹道速度/画布高度
    "fire_speed": 0.5,

    //开场后的冷静时间(多少秒内不能攻击)
    "calm_time": 4,

    //随机粒子数量,20 + Math.random() * 10
    //最小为[0,1],是无粒子
    "particle_number":[20,10],

    //火球的攻击范围/画布高度
    "fireball_range" : 5,

    //火球技能的伤害/画布高度
    "fireball_damage": 0.01,

    //火球的颜色
    "fireball_color":"orange",

    //火球技能的伤害(被攻击后减移速的比例)
    "reduce_ratio": 0.8,

    //玩家的死亡大小
    "dead_szie": 10,




//------------------------playground/zbase.js-------------------------
    //玩家初始大小百分比(相对于浏览器的宽)
    "players_size_percent": 0.05,

    //玩家自己的颜色
    "self_color": "white",

    //玩家的移动速度,用每秒移动高度的百分比表示
    "player_speed_percent": 0.15,

    //电脑玩家的数量
    "AIs_number": 5,

    //所有玩家的颜色列表
    "color_select": ["#c66f35", "gree", "#c0d6cb", "#1cce31", "#9fa0d7", "#cc99ff"]
//------------------------------------------------------------------------
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值