本节内容是游戏界面的设计,包括各个目标的绘制、角色的移动与攻击、AI 敌人的设计等部分。
1. 模块化引入JS变量
首先我们需要对之前的代码进行一点小修改,在 web.html
中使用 <script>
会导致定义的所有 Class(例如 AcGame
)都会变成网页的全局变量,当引入多个 JS 文件后网页可能会出现重名变量导致冲突,我们最好做一个模块化,当我们需要某个名称的时候,我们只将这一个名称引入进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {% load static %} <head > <link rel ="stylesheet" href ="http://8.130.54.44:8000/static/css/jquery-ui.min.css" > <script src ="http://8.130.54.44:8000/static/js/jquery-3.6.1.min.js" > </script > <link rel ="stylesheet" href ="{% static 'css/game.css' %}" > </head > <body style ="margin: 0" > <div id ="ac_game_1" > </div > <script type ="module" > import {AcGame } from "{% static 'js/dist/game.js' %}" ; $(document ).ready (function ( ) { let ac_game = new AcGame ("ac_game_1" ); }); </script > </body >
这时候我们刷新网页会看到报错:Uncaught SyntaxError: The requested module '/static/js/dist/game.js' does not provide an export named 'AcGame'
,表示如果我们想加载 AcGame
的话需要在这个类前面加一个关键字 export
:
1 2 3 4 5 6 7 8 9 10 11 12 13 export class AcGame { constructor (id ) { this .id = id; this .$ac_game = $('#' + id); this .menu = new AcGameMenu (this ); this .playground = new AcGamePlayground (this ); this .start (); } start ( ) { } }
此时再次刷新网页即可看到报错消失。
2. 实现物体运动基类
在游戏中物体的运动效果是通过不断刷新界面实现的,浏览器每秒刷新60次(即60帧),每一帧都是一张图片,因此我们需要先实现一个能够每一帧都调用对象的刷新函数的基类(简易游戏引擎)。
我们在 static/js/src/playground/
目录下创建一个 ac_game_object
目录,然后进入该目录创建 zbase.js
。
这个类一般有三个函数,函数 start
在开始时执行一次,用于创建对象时初始化对象的颜色、分值、昵称等信息(从服务器端加载出来),函数 update
每一帧都会执行一次,函数 destroy
表示删除当前对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 let AC_GAME_OBJECTS = []; class AcGameObject { constructor ( ) { AC_GAME_OBJECTS .push (this ); } 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 ; } } } }
然后我们需要实现每一帧循环渲染一遍全局数组中的对象,JS 提供了一个 API:requestAnimationFrame()
,该函数会在一秒钟调用60次,也就是将一秒钟分成60等份,在下一帧时执行一遍函数,我们可以定义一个函数 AC_GAME_ANIMATION
表示每帧需要执行的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 let AC_GAME_OBJECTS = []; class AcGameObject { constructor ( ) { AC_GAME_OBJECTS .push (this ); this .has_called_start = false ; 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 ) { 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 );
3. Canvas绘制游戏画面
接下来我们需要创建游戏画面,在 playground
目录下创建一个 game_map
目录,然后创建 zbase.js
:
1 2 3 4 5 6 7 8 9 10 11 class GameMap extends AcGameObject { constructor (playground ) { 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 ; this .playground .$playground .append (this .$canvas ); } }
在 playground
目录下的 zbase.js
中将游戏画面对象创建出来即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class AcGamePlayground { constructor (root ) { this .root = root; this .$playground = $(` <div class='ac_game_playground'> </div> ` ); this .root .$ac_game .append (this .$playground ); this .width = this .$playground .width (); this .height = this .$playground .height (); this .game_map = new GameMap (this ); this .start (); } start ( ) { this .hide (); } show ( ) { this .$playground .show (); } hide ( ) { this .$playground .hide (); } }
现在浏览器中的 Canvas 是没有颜色的,我们需要将它渲染出来,即在 GameMap
类中添加一个渲染函数 render
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class GameMap extends AcGameObject { constructor (playground ) { ... } start ( ) { } update ( ) { this .render (); } render ( ) { this .ctx .fillStyle = 'rgba(0, 0, 0, 0.2)' ; this .ctx .fillRect (0 , 0 , this .ctx .canvas .width , this .ctx .canvas .height ); } }
4. 创建游戏角色及实现移动效果
在 playground
目录下创建一个 player
目录,然后创建 zbase.js
(角色也是一个游戏对象,因此也要从 AcGameObject
类中扩展出来),角色需要传入中心坐标 x, y
、半径 radius
、颜色 color
、每秒移动的距离百分比 speed
、是否为自己 is_me
(因为未来在联机的时候自己和敌人的操作方式是不一样的,敌人的操作是通过网络传过来的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 .radius = radius; this .color = color; this .speed = speed; this .is_me = is_me; this .eps = 0.1 ; } 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 (); } }
然后我们修改 AcGamePlayground
类将自己创建出来试试效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class AcGamePlayground { constructor (root ) { this .root = root; this .$playground = $(` <div class='ac_game_playground'> </div> ` ); 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 )); this .start (); } start ( ) { this .hide (); } show ( ) { this .$playground .show (); } hide ( ) { this .$playground .hide (); } }
接下来我们实现小球的移动,我们需要给每个小球设置一个 X 轴方向的速度和 Y 轴方向的速度,在每次刷新对象的时候更新一下小球的坐标即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Player extends AcGameObject { constructor (playground, x, y, radius, color, speed, is_me ) { ... this .vx = 0.5 ; this .vy = 0.5 ; } start ( ) { } update ( ) { this .x += this .vx ; this .y += this .vy ; this .render (); } render ( ) { ... } }
现在我们还需要实现小球移动到鼠标右键点击的位置,如下图所示,假设从 (x, y)
移动到 (tx, ty)
,移动的距离即为两点间的欧几里得距离,atan2(y, x)
函数可以求出方向角 θ,我们将其移动方向归一化视为一个单位圆,那么 X 轴方向的速度即为 1 * cos(θ)
,Y 轴方向的速度即为 1 * sin(θ)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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 ; this .vy = 0 ; this .move_length = 0 ; this .radius = radius; this .color = color; this .speed = speed; this .is_me = is_me; this .eps = 0.1 ; } start ( ) { if (this .is_me ) { this .add_listening_events (); } } 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 ); } }); } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } move_to (tx, ty ) { this .move_length = this .get_dist (this .x , this .y , tx, ty); let theta = Math .atan2 (ty - this .y , tx - this .x ); this .vx = Math .cos (theta); this .vy = Math .sin (theta); } update ( ) { if (this .move_length < this .eps ) { this .move_length = 0 ; this .vx = this .vy = 0 ; } else { let true_move = Math .min (this .move_length , this .speed * this .timedelta / 1000 ); this .x += this .vx * true_move; this .y += this .vy * true_move; this .move_length -= true_move; } 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 (); } }
5. 创建角色技能
在 playground
目录下创建一个 skill
目录,我们先实现火球技能,在 skill
目录下再创建 fireball
目录,然后创建 zbase.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class FireBall extends AcGameObject { constructor (playground, player, x, y, radius, vx, vy, color, speed, move_length ) { 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 .eps = 0.1 ; } start ( ) { } update ( ) { if (this .move_length < this .eps ) { this .destroy (); return false ; } let true_move = Math .min (this .move_length , this .speed * this .timedelta / 1000 ); this .x += this .vx * true_move; this .y += this .vy * true_move; this .move_length -= true_move; 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 (); } }
然后我们要实现玩家选中某个技能后点击鼠标能够使用该技能,获取按键信息时不能用 Canvas,因为不能聚焦,可以用 Window 来获取,每个按键的编号可以在网上查找对照表获得:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 class Player extends AcGameObject { constructor (playground, x, y, radius, color, speed, is_me ) { ... this .cur_skill = null ; } start ( ) { if (this .is_me ) { this .add_listening_events (); } } 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 ); } 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 ) { outer.cur_skill = 'fireball' ; return false ; } }); } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } shoot_fireball (tx, ty ) { let x = this .x , y = this .y ; let radius = this .playground .height * 0.01 ; let theta = Math .atan2 (ty - this .y , tx - this .x ); let vx = Math .cos (theta), vy = Math .sin (theta); let color = 'orange' ; let speed = this .playground .height * 0.5 ; let move_length = this .playground .height * 0.8 ; new FireBall (this .playground , this , x, y, radius, vx, vy, color, speed, move_length); } move_to (tx, ty ) { ... } update ( ) { ... } render ( ) { ... } }
6. 创建敌人及实现简单AI
首先我们在 AcGamePlayground
类中添加5名敌人:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class AcGamePlayground { constructor (root ) { ... for (let i = 0 ; i < 5 ; i++) { this .players .push (new Player (this , this .width / 2 , this .height / 2 , this .height * 0.05 , 'blue' , this .height * 0.15 , false )); } this .start (); } start ( ) { this .hide (); } show ( ) { this .$playground .show (); } hide ( ) { this .$playground .hide (); } }
然后我们需要让敌人移动起来,可以设定一个随机的目的地,然后移动到该目的地时再随机一个新的目的地,在 Player
中进行相应的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 class Player extends AcGameObject { constructor (playground, x, y, radius, color, speed, is_me ) { ... } 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 ( ) { ... } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } shoot_fireball (tx, ty ) { ... } move_to (tx, ty ) { this .move_length = this .get_dist (this .x , this .y , tx, ty); let theta = Math .atan2 (ty - this .y , tx - this .x ); this .vx = Math .cos (theta); this .vy = Math .sin (theta); } 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 true_move = Math .min (this .move_length , this .speed * this .timedelta / 1000 ); this .x += this .vx * true_move; this .y += this .vy * true_move; this .move_length -= true_move; } this .render (); } render ( ) { ... } }
7. 火球碰撞检测
两个圆的相交检测很简单,只需要判断两个圆的圆心距离是否小于两圆的半径之和即可,我们在 FireBall
类中进行碰撞检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class FireBall extends AcGameObject { constructor (playground, player, x, y, radius, vx, vy, color, speed, move_length, damage ) { ... this .damage = damage; this .eps = 0.1 ; } start ( ) { } update ( ) { if (this .move_length < this .eps ) { this .destroy (); return false ; } let true_move = Math .min (this .move_length , this .speed * this .timedelta / 1000 ); this .x += this .vx * true_move; this .y += this .vy * true_move; this .move_length -= true_move; for (let i = 0 ; i < this .playground .players .length ; i++) { let player = this .playground .players [i]; if (player !== this .player && this .is_collision (player)) { this .attack (player); } } this .render (); } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } 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 theta = Math .atan2 (player.y - this .y , player.x - this .x ); player.is_attacked (theta, this .damage ); this .destroy (); } render ( ) { ... } }
接着我们需要实现玩家被攻击时的效果,被攻击时有向后的击退效果,且击退过程中玩家无法移动,被攻击后血量减少(使用小球半径表示血量,即被攻击后小球半径变小),血量越少移动速度越快,我们在 Player
类中进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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 ; this .vy = 0 ; this .damage_vx = 0 ; this .damage_vy = 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 .cur_skill = null ; } start ( ) { ... } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } shoot_fireball (tx, ty ) { ... } move_to (tx, ty ) { ... } is_attacked (theta, damage ) { this .radius -= damage; this .speed *= 1.08 ; if (this .radius < 10 ) { this .destroy (); return false ; } this .damage_vx = Math .cos (theta); this .damage_vy = Math .sin (theta); this .damage_speed = damage * 90 ; } update ( ) { if (this .damage_speed > this .eps ) { this .vx = this .vy = 0 ; this .move_length = 0 ; this .x += this .damage_vx * this .damage_speed * this .timedelta / 1000 ; this .y += this .damage_vy * 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 true_move = Math .min (this .move_length , this .speed * this .timedelta / 1000 ); this .x += this .vx * true_move; this .y += this .vy * true_move; this .move_length -= true_move; } } this .render (); } render ( ) { ... } }
8. 实现被攻击时的粒子效果
我们可以设计一个当被攻击时向前爆出若干小球的粒子效果,在 playground
目录下创建一个 particle
目录,然后创建 zbase.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 .eps = 1 ; this .friction = 0.9 ; } start ( ) { } update ( ) { if (this .move_length < this .eps || this .speed < this .eps ) { this .destroy (); return false ; } let true_move = Math .min (this .move_length , this .speed * this .timedelta / 1000 ); this .x += this .vx * true_move; this .y += this .vy * true_move; this .speed *= this .friction ; this .move_length -= true_move; 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 (); } }
然后在 Player
类的被击中函数中创建若干粒子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class Player extends AcGameObject { constructor (playground, x, y, radius, color, speed, is_me ) { ... } start ( ) { ... } add_listening_events ( ) { ... } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } shoot_fireball (tx, ty ) { ... } move_to (tx, ty ) { ... } is_attacked (theta, damage ) { for (let i = 0 ; i < 10 + Math .random () * 5 ; i++) { let x = this .x , y = this .y ; let radius = this .radius * Math .random () * 0.2 ; let theta = Math .PI * 2 * Math .random (); let vx = Math .cos (theta), vy = Math .sin (theta); let color = this .color ; let speed = this .speed * 10 ; let move_length = this .radius * Math .random () * 10 ; new Particle (this .playground , x, y, radius, vx, vy, color, speed, move_length); } this .radius -= damage; this .speed *= 1.08 ; if (this .radius < 10 ) { this .destroy (); return false ; } this .damage_vx = Math .cos (theta); this .damage_vy = Math .sin (theta); this .damage_speed = damage * 90 ; } update ( ) { ... } render ( ) { ... } }
9. 敌人随机颜色
最后我们随机生成每个敌人的颜色,在 Playground
类中进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class AcGamePlayground { constructor (root ) { ... 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 ( ) { this .hide (); } show ( ) { this .$playground .show (); } hide ( ) { this .$playground .hide (); } }
10. 敌人自动攻击
我们可以设置敌人随机向玩家射击,但是开局前几秒限制敌人射击,修改 Player
类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class Player extends AcGameObject { constructor (playground, x, y, radius, color, speed, is_me ) { ... this .spent_time = 0 ; this .cur_skill = null ; } start ( ) { ... } add_listening_events ( ) { ... } get_dist (x1, y1, x2, y2 ) { return Math .sqrt ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } shoot_fireball (tx, ty ) { ... } move_to (tx, ty ) { ... } is_attacked (theta, damage ) { ... } update ( ) { this .spent_time += this .timedelta / 1000 ; if (this .spent_time > 3 && !this .is_me && Math .random () < 1 / 360.0 ) { let player = this .playground .players [0 ]; this .shoot_fireball (player.x , player.y ); } ... } render ( ) { ... } }
上一章:Django学习笔记-创建菜单界面 。
下一章:Django学习笔记-部署Nginx与对接AcApp 。