三、实现游戏界面

这是实现之后的效果:

        

游戏界面展示

----------创建游戏界面


1.修改上节课的不足之处:
    web.html中的z会作为全局的全局变量,会很不方便
    我们将这一行删掉,在body中把script的tpye改成module,这样浏览器就会把引入的脚本识别为 JS module,且需要什么就引用什么,如下:
         `

    然后要将这个类名暴露(export)出来,也即在/game/static/js/src/zbase.js的class AcGame前面加上export,即

    export class AcGame
    {
        ......
    }

    若出现SyntaxError: Unexpected token export报错,则将上述操作重复操作一遍即可。

----------开始写单人模式


1.为了调试方便,我们先将菜单页面关闭。
    在/game/static/js/src/zbase.js的class中,将创建菜单注释掉
    然后在AcGamePlayground中,将游戏界面隐藏给注释掉,即可

2.打开game/static/js/src/playground/zbase.js
     `class AcGamePlayground {
    constructor(root) {
        this.root = root;
        this.$playground = $(`

`); #新标签,需要重新加上样式

        // this.hide();
        this.root.$ac_game.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 ++ ) {
            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();
    }
}


 然后为新标签加上样式:
  `.ac-game-playground
{
    width: 100%; // 宽度
    height: 100%; // 高度
    user-select: none; //鼠标禁用菜单
}` 

3.移动播放原理:每秒刷新60次,即每秒播放60张图,每一张图都会移动一点点,按照秒数去移动的时候,看起来就像是在移动一样。实现动的概念的时候,就是每秒钟重新画一张图,然后小球的坐标,每张图都会移动一点点,这样小球就可以动了。每一张图片被称为每一帧。

4.实现一个基类,即简易的游戏引擎:
    在js/src/playground/下创建ac_game_object/zbase.js,内容如下:
     `let AC_GAME_OBJECTS = []; //将创建的物体放进一个全局数组,每一秒调用该数组里的对象60次即可

class AcGameObject {
    constructor() {
        AC_GAME_OBJECTS.push(this); //每一次创建都将对象直接加入全局数组

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

    start() {  // 只会在第一帧执行一次
    }

    update() {  // 每一帧均会执行一次
    }

    on_destroy() {  // 在被销毁前执行一次
    }

    destroy() {  // 删掉该物体
        this.on_destroy();

        //在js中,当物体没有被变量存下来时,就会被自动释放掉
        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];
        if (!obj.has_called_start) {
            obj.start();
            obj.has_called_start = true;
        } else {
            obj.timedelta = timestamp - last_timestamp; //当前物体的时间间隔等于当前的时间戳-上一帧的时间戳
            obj.update();
        }
    }
    last_timestamp = timestamp;  //更新上一秒的时间戳

    requestAnimationFrame(AC_GAME_ANIMATION);  //利用递归,实现每一帧都调用一次该函数
}


requestAnimationFrame(AC_GAME_ANIMATION); //js提供的api,会把一秒分成60帧,会在下一帧之前,调用一下该函数,同时把调用的时间timestamp传给该函数


    该类,每一帧都会渲染一次物体

4.写游戏地图:
    在js/src/playground/game_map/zbase.js中写,用HTML里面的canvas画布渲染。(canvas菜鸟教程可供参考)
        内容如下:
             `class GameMap extends AcGameObject {//GameMap为子类(派生类),AcGameObject为父类(基类),派生类可以调用基类的函数(首先判断自己是否有这个函数,若无,则调用基类中的函数)
    constructor(playground) {  //传人了一个类
        super();  //调用一下基类的构造函数
        this.playground = playground;  //将画布存下来未来会用到,
        this.$canvas = $(``); //将画布存下来
        this.ctx = this.$canvas[0].getContext('2d');  //未来操作是操作这个Context
        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)"; //黑色 20%透明度
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);//参数为x坐标 y坐标 宽度 高度
    }
}

5.写玩家:
     `class Player extends AcGameObject { //该类作为子类
    constructor(playground, x, y, radius, color, speed, is_me) { //游戏界面,中心坐标 半径 颜色 速度 是否为自己
        super(); //调用基类构造函数,存进全局数组,每秒调用60次
        this.playground = playground;
        this.ctx = this.playground.game_map.ctx; //找到画布
        this.x = x; //玩家坐标
        this.y = y;
        this.vx = 0; //x方向速度
        this.vy = 0;  //y方向速度
        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; //浮点运算,小于该值视为零
        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) { //右键
                outer.move_to(e.clientX, e.clientY); //鼠标坐标的api
            } 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) {  // q
                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); //每次打掉玩家20%的血量
    }

    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) { //移动
        this.move_length = this.get_dist(this.x, this.y, tx, ty);
        let angle = Math.atan2(ty - this.y, tx - this.x);  //atan2求角度 偏移量:所以应该是鼠标坐标减去玩家坐标
        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) { //玩家半径小于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; //计算过去了多久
        //游戏开始5秒后再开始攻击  当生成的随机数小于300分之一时,就攻击一次 需要判断是否为人机
        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;  //速度归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 {
                //当前每秒的速度 / 1000 * 时间差 = 这段时间差需要移动的距离 (这是按照当前速度需要移动的距离)但不能出界,需要与实际需要移动的距离取最小值
                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();
    }

    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);//注销玩家
            }
        }
    }
}

6.写火球技能:
    在game/static/js/src/playground/skill/fireball/zbase.js中写,内容如下:
         `class FireBall extends AcGameObject {
    constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, 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();
    }
}

7.写被攻击后的粒子效果:
    在game/static/js/src/playground/animation/particle/zbase.js下写,内容如下:

     `class Particle extends AcGameObject {
    constructor(playground, x, y, radius, vx, vy, color, speed, move_length) {
        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() {
        //判断速度有没有变成很小的值,或者需要移动距离为 0时
        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; //无线趋近于0
        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();
    }
}

`

你可能感兴趣的:(django笔记,游戏,servlet,javascript)