AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing《Django框架课》第 4 讲 创建游戏界面(详细操作步骤)

作者: 作者的头像   tonngw ,  2023-03-30 15:11:51 ,  所有人可见 ,  阅读 276


1


前一讲: 创建菜单界面(详细操作步骤)https://www.acwing.com/blog/content/33077/

前端 JS 模块化改造

将原来全局的 JS 定义改造成模块化的形式,缩小作用域,同时防止之后项目变大之后变量重名等。

删除 web.html 中的全局引入文件 game.js,在 js 代码中声明 script 的 type="module",修改后的代码

{% 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_gmae = new AcGame("ac_game_12345678");
        });
        console.log("hello acapp");
    </script>
</body>

在总的 zbase.js 中导出 AcGame,在类声明前添加 export 关键字

export class AcGame {}

编写游戏界面逻辑

改完记得执行脚本打包代码

修改 playground/zbase.js

this.$playground = $(`<div class="ac-game-playground"></div>`);

添加样式 game.css

.ac-game-playground {
    witdh: 100%;
    height: 100%;
    user-select: none;
}

实现一个简易的游戏引擎

y 总金句:不要依赖 IDE,导致未来发展很受局限。

之后的所有对象(地图、玩家、技能等)都继承这个游戏引擎对象,拥有里面的处理方法,可以使用父类的,可以自己重载实现。

let AC_GAME_OBJECTS = [];

class AcGameObject {
    constructor() {
        AC_GAME_OBJECTS.push(this);

        this.has_called_start = false; // 是否执行过 start() | run start()?
        this.timedelta = 0; // 两帧之间的时间间隔 ms | time interval of two frames
    }

    start() { // 只会在第一帧执行一次 | only run once at first frame
    }

    update() { // 每一帧都会执行一次 | run once every frame
    }

    on_destroyed() { // 在被销毁前执行一次 | run once before delete
    }

    destroy() { // 删除该物体 | delete it
        this.on_destory();

        for (let i = 0; i < AC_GAME_OBJECTS.length; i ++ ) {
            if (AC_GAME_OBJECTS[i] === this) {
                AC_GAME_OBJECTS.splice(i, 1); // 从 i 开始删除一个 | delete one element from i pos
                break;
            }
        }
    }
}

let last_timestamp; // 上一帧的时间戳

// 渲染每一帧之后的回调函数 | render every frame callback function
let AC_GAME_ANIMATION = function(timestamp) {
    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); // 递归渲染 | recursive render
}

requestAnimationFrame(AC_GAME_ANIMATION); // 第一帧图像渲染一次之后,调用回调函数 | run once invoke funtion

右键移动小球

没有效果 Ctrl + F5 刷新页面

game_map/zbase.js,创建地图

class GameMap extends AcGameObject {
    constructor(playground) {
        super(); // invoke extends class constructor funtion
        this.playground = playground;
        this.$canvas = $(`<canvas></canvas>`); // create canvas
        this.ctx = this.$canvas[0].getContext('2d'); // 2d canvas
        this.ctx.canvas.width = this.playground.width;
        this.ctx.canvas.height = this.playground.height;
        this.playground.$playground.append(this.$canvas);
    }

    start() {
    }

    update() {
        this.render(); // 每一帧画一次 | every frame paint once
    }

    render() {
        this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

player/zbase.js,创建玩家

class Player extends AcGameObject {
    constructor(playground, x, y, radius, color, speed, is_me) {
        super();
        this.playground = playground;
        this.ctx = this.playground.game_map.ctx;
        this.x = x;
        this.y = y;
        this.vx = 0; // x direction speed, unit is px here
        this.vy = 0; // y direction speed
        this.move_length = 0; // need to move distance
        this.radius = radius;
        this.color = color;
        this.speed = speed;
        this.is_me = is_me;
        this.eps = 0.1;
    }

    start() {
        if (this.is_me) { // add a mouse listen events only for me
            this.add_listening_events();
        }
    }

    add_listening_events() {
        let outer = this;
        // cancel mouse right click menu
        this.playground.game_map.$canvas.on("contextmenu", function() {
            return false;
        });
        // get mouse down x and y coordinate
        this.playground.game_map.$canvas.mousedown(function(e) {
            if (e.which === 3) { // 3 is mouse down right key
                outer.move_to(e.clientX, e.clientY);
            }
        });
    }

    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) {
        // console.log("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); // 计算角度 | use arctan function compute angle
        this.vx = Math.cos(angle); // compute vx and vy
        this.vy = Math.sin(angle);
    }

    update() {
        if (this.move_length < this.eps) { // 如果已经移动到了指定位置就停止移动| if moved to the specific pos, it stops moving.
            this.move_length = 0;
            this.vx = this.vy = 0;
        } else {
            let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // 计算 timedelta 时间间隔需要真实移动的距离(最后和 move_length 取最小值,防止移动出界) | compute real moving distance in timedelta time intervals.
            this.x += this.vx * moved;
            this.y += this.vy * moved;
            this.move_length -= moved;
            // console.log(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();
    }
}

总zbase.js,添加地图和玩家

class AcGamePlayground {
    constructor(root) {
        this.root = root;
        this.$playground = $(`<div class="ac-game-playground"></div>`);

        // this.hide(); // 刚开始游戏界面是隐藏的 | playground page is hidden at first
        this.root.$ac_game.append(this.$playground);
        // 记录界面的宽度和高度,后面经常使用
        this.width = this.$playground.width();
        this.height = this.$playground.height();
        this.game_map = new GameMap(this); // 创建游戏地图 | create game_map
        this.players = [];        
        this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true)); // 创建玩家 | create player

        this.start();
    }

    start() {
    }

    show() { // 打开 playground 界面 | open playground page
        this.$playground.show();
    }

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

到这里实现的效果就是点击鼠标右键小球可以移动到指定位置。

碰撞效果

  • 碰撞检测
  • 撞击后移
  • 粒子效果

JS 是真难调试啊!!!

this.damage_speed 属性少些了一个 a 找了半天…,因为是属性它认为你是新创建的,也不会报错,导致你的速度没有减慢,一直朝着一个方向跑。

完整代码地址:https://git.acwing.com/tonngw/acapp/-/commit/d7db4ec311252ab94265fae7fe31e0c1b0cb8364,核心部分完整代码

playground/zbase.js

class AcGamePlayground {
    constructor(root) {
        this.root = root;
        this.$playground = $(`<div class="ac-game-playground"></div>`);

        // this.hide(); // 刚开始游戏界面是隐藏的 | playground page is hidden at first
        this.root.$ac_game.append(this.$playground);
        // 记录界面的宽度和高度,后面会经常使用
        this.width = this.$playground.width();
        this.height = this.$playground.height();
        this.game_map = new GameMap(this); // 创建游戏地图 | create game_map
        this.players = [];
        this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true)); // 创建玩家 | create player

        // 创建 5 个敌人 | create enemy
        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 color = ["blue", "red", "pink", "grey", "green"];
        return color[Math.floor(Math.random() * 5)];
    }

    start() {
    }

    show() { // 打开 playground 界面 | open playground page
        this.$playground.show();
    }

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

playground/game_map/zbase.js

class GameMap extends AcGameObject {
    constructor(playground) {
        super(); // invoke extends class constructor funtion
        this.playground = playground;
        this.$canvas = $(`<canvas></canvas>`); // create canvas
        this.ctx = this.$canvas[0].getContext('2d'); // 2d canvas
        this.ctx.canvas.width = this.playground.width;
        this.ctx.canvas.height = this.playground.height;
        this.playground.$playground.append(this.$canvas);
    }

    start() {
    }

    update() {
        this.render(); // 每一帧画一次 | every frame paint once
    }

    render() {
        this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

playground/player/zbase.js

class Player extends AcGameObject {
    constructor(playground, x, y, radius, color, speed, is_me) {
        super();
        this.playground = playground;
        this.ctx = this.playground.game_map.ctx;
        this.x = x;
        this.y = y;
        this.vx = 0; // x direction speed, unit is px here
        this.vy = 0; // y direction speed
        this.damage_x = 0; // player is attacked x、y and speed
        this.damage_y = 0;
        this.damage_speed = 0;
        this.move_length = 0; // need to move distance
        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; // 冷静时间,冷静之内 AI 不发射炮弹

        this.cur_skill = null; // 当前选择的技能,为空就是没选 | current select skill, default null
    }

    start() {
        if (this.is_me) { // if it is itelf, add a mouse listen events only for me
            this.add_listening_events();
        } else { // else it is enemy, random generate coordinate (tx, ty)
            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;
        // cancel mouse right click menu
        this.playground.game_map.$canvas.on("contextmenu", function() {
            return false;
        });
        // get mouse down x and y coordinate
        this.playground.game_map.$canvas.mousedown(function(e) {
            if (e.which === 3) { // 3 is mouse down right key
                outer.move_to(e.clientX, e.clientY);
            } else if (e.which === 1) { // 2 is mouse down left key
                if (outer.cur_skill === "fireball") { // press mouse left key and Q key shoot firebal
                    outer.shoot_fireball(e.clientX, e.clientY);
                }

                outer.cur_skill = null; // clear cur_skill flag after release skill
            }
        });


        $(window).keydown(function(e) { // 添加键盘监听事件 | add keyboard listening events
            if (e.which === 81) { // Q key
                outer.cur_skill = "fireball";
                return false;
            }
        });
    }

    shoot_fireball(tx, ty) {
        let x = this.x, y = this.y; // fireball coordiate
        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; // 技能释放的长度 | release skill length
        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) {
        // console.log("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); // 计算角度 | use arctan function compute angle
        this.vx = Math.cos(angle); // compute vx and vy
        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; // 减慢被攻击的玩家的速度 | slow down attacked player speed
    }

    update() {
        this.spent_time += this.timedelta / 1000;
        // 不是自己 && 5s 钟之后再进行攻击 && 朝着玩家每 5s 发射一枚炮弹 | shoot a fireball for player(not ai player) in every 3 second
        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; // ai 预判玩家下一帧的位置,增加游戏难度
            let ty = player.y + player.speed * this.vy * this.timedelta / 1000 * 0.3;
            this.shoot_fireball(player.x, player.y);
        }

        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) { // 如果已经移动到了指定位置就停止移动| if moved to the specific pos, it stops moving.
                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); // 计算 timedelta 时间间隔内需要真实移动的距离(最后和 move_length 取最小值,防止移动出界) | compute real moving distance in timedelta time intervals.
                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);
            }
        }
    }
}

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) { // 火球移动结束,销毁火球 | fireball move end and destroy it
            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();
    }

}

playground/particle/zbase.js

class Particle extends AcGameObject { // 粒子效果 | particle effect
    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() {
        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; // speed slow down
        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();
    }
}

实验结果

03_第4讲游戏界面.gif

0 评论

App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息