Django 框架课
4. 创建游戏界面
首先是完善上节课的内容
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' %}">
<script src="{% static 'js/dist/game.js' %}"></script> <!--看这里-->
</head>
<body style="margin: 0">
<div id="ac_game_01"></div>
<script>
$(document).ready(function(){
let ac_game = new AcGame("ac_game_01");
});
</script>
</body>
在这里,引用game.js
的方式有问题,如果像<script src="{% static 'js/dist/game.js' %}"></script>
这样引用,这样会导致这个引入的类是全局的。我们最好用模块的方式引入进来。改成如下形式:
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_01"></div>
<script>
import {AcGame} from "{% static 'js/dist/game.js' %}" <!--看这里-->
$(document).ready(function(){
let ac_game = new AcGame("ac_game_01");
});
</script>
</body>
然后要将这个类名暴露(export)出来,也即在/game/static/js/src/zbase.js
的class AcGame
前面加上export
即可。
zbase.js
export class AcGame
{
...
}
接下来就是写游戏界面
前排提醒,代码不要光看,要边看边一行一行照着抄边思考,不要直接复制,否则没有学习效果。或者可以先看本教程的代码和y总的代码,觉得哪边更好就抄哪边的代码。
前排提醒,这里的教程上,有很多部分代码看上去跟y总的代码不一样,但实际上是笔者在结合y总教的之后,对这些代码的整理和优化,这是为了以后维护代码能更加轻松,不至于面向这些代码不知所措,同时更加符合逻辑。
前排提醒,代码里面的一些值如果自己调试的时候觉得不是很合适,可以自己调一调,直到觉得合适,比如攻击力、击退速度等等。
前排提醒,一开始写代码,出现bug是不可避免的,有时候会花费很多时间去debug,但是这个过程是必须经历的,否则以后进行工作量更大的开发,不会debug就完蛋了。debug方式可以参考y总。
写游戏界面首先打开game/static/js/src/playground/zbase.js
,
zbase.js
class AcGamePlayground
{
constructor(root)
{
this.root = root;
this.$playground = $(`
<div class="ac-game-playground"></div>
`); // 这里定义了新HTML类
this.root.$ac_game.append(this.$playground);
this.width = this.$playground.width; // 领域的宽度
this.height = this.$playground.height; // 领域的高度
this.$back = this.$playground.find('.ac-game-playground-item-back')
this.start();
}
add_listening_events()
{
let outer = this;
this.$back.click(function(){
outer.hide();
outer.root.$menu.show();
});
}
show()
{
this.$playground.show();
}
hide()
{
this.$playground.hide();
}
start()
{
this.hide();
this.add_listening_events();
}
update()
{
}
}
定义HTML新类的时候同样要在game.css
;里面修改css样式。在game.css
里面加上如下代码。
.ac-game-playground
{
width: 100%; // 宽度
height: 100%; // 高度
user-select: none; // 禁用右键弹菜单
}
后面就是纯JS了
首先要了解动画的基本原理,懂的都懂。也就是只要一堆图片,只要切换图片的速度够快,人眼上看就像是动态的。
那么这个动态的东西(对象),我们可以定义为AcGameObject
,这个就是可以“动”的东西。可以作为以后可以“动”的对象的基类。所以我们在js/src/playground/
创建ac_game_object/zbase.js
。定义AcGameObject
类。
zbase.js
let AC_GAME_OBJECTS = []; // 储存所有可以“动”的元素的全局数组
class AcGameObject
{
constructor(hurtable = false) // 构造函数
{
AC_GAME_OBJECTS.push(this); // 将这个对象加入到储存动元素的全局数组里
this.has_call_start = false; // 记录这个对象是否已经调用了start函数
this.timedelta = 0; // 当前距离上一帧的时间间隔,相等于时间微分,用来防止因为不同浏览器不同的帧数,物体移动若按帧算会不同,所以要用统一的标准,就要用时间来衡量
this.hurtable = hurtable; // 决定这个元素能否被碰撞,默认为不能
}
start()
{
// 只会在第一帧执行一次的过程
}
update()
{
// 每一帧都会执行的过程
}
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); // 从数组中删除元素的函数splice()
break;
}
}
}
on_destroy()
{
// 被删除之前的过程,“临终遗言”
}
}
let last_timestp; // 上一帧的时间
let AC_GAME_ANIMATION = function(timestp) // timestp 是传入的一个参数,就是当前调用的时间
{
for (let i = 0; i < AC_GAME_OBJECTS.length; ++ i) // 所有动的元素都进行更新。
{
let obj = AC_GAME_OBJECTS[i];
if (!obj.has_called_start)
{
obj.start(); // 调用start()
obj.has_called_start = true; // 表示已经调用过start()了
}
else
{
obj.timedelta = timestp - last_timestp; // 时间微分
obj.update(); // 不断调用
}
}
last_timestp = timestp; // 进入下一帧时当前时间戳就是这一帧的时间戳
requestAnimationFrame(AC_GAME_ANIMATION); // 不断递归调用
}
requestAnimationFrame(AC_GAME_ANIMATION); // JS的API,可以调用1帧里面的函数。(有些浏览器的一秒帧数不一定相等)
接下来写游戏地图GameMap
,这个地图也是会随时更新的“动”的元素,所以要用AcGameObject
。以后会动的元素都以AcGameObject
为基类,不再赘述。在js/src/playground/game_map/zbase.js
。
我们用HTML里面的canvas画布渲染。(想对canvas了解深入建议查询菜鸟教程)
zbase.js
class GameMap extends AcGameObject
{
constructor(playground)
{
super(); // 调用基类的构造函数
this.playground = playground; // 这个Map是属于这个playground的
this.$canvas = $(`<canvas></canvas>`); // canvas是画布
this.ctx = this.$canvas[0].getContext('2d'); // 用ctx操作画布canvas
this.ctx.canvas.width = this.playground.width; // 设置画布的宽度
this.ctx.canvas.height = this.playground.height; // 设置画布的高度
this.playground.$playground.append(this.$canvas); // 将这个画布加入到这个playground
}
render()
{
this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)"; // 填充颜色设置为透明的黑色
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); // 画上给定的坐标的矩形
}
start()
{
}
update()
{
this.render(); // 这个地图要一直画一直画(动画的基本原理)
}
}
接下来写玩家类
玩家类也是游戏会“动”的元素,所以也应该…(不说了)。
class Player extends AcGameObject
{
constructor(playground, x, y, radius, color, is_me, speed)
{
super(true);
this.playground = playground; // 所属playground
this.ctx = this.playground.game_map.ctx; // 操作的画笔
this.x = x; // 坐标
this.y = y; // 坐标
this.radius = radius; // 半径
this.color = color; // 颜色
this.is_me = is_me; // 玩家类型
this.speed = speed; // 速度
this.is_alive = true; // 是否存活
this.eps = 0.1; // 精度,这里建议定义为全局变量,EPS = 0.1,在这个教程里以后都这么用。
}
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();
}
start()
{
}
update()
{
this.render(); // 同样要一直画一直画(yxc:“人不吃饭会死,物体不一直画会消失。”)
}
on_destroy() // 死之前在this.playground.players数组里面删掉这个player
{
this.is_alive = false; // 已经去世了
for (let i = 0; i < this.playground.players.length; ++ i)
{
let player = this.playground.players[i];
if (this === player)
{
this.playground.players.splice(i, 1);
}
}
}
}
准备好了这些,接下来更新一下AcGamePlayground
。下面是js/src/playground/zbase.js
的代码。
zbase.js
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", true, this.height * 0.15)); // 创建一个是自己的玩家
this.$back = this.$playground.find('.ac-game-playground-item-back')
this.start();
}
add_listening_events()
{
let outer = this;
this.$back.click(function(){
outer.hide();
outer.root.$menu.show();
});
}
show()
{
this.$playground.show();
}
hide()
{
this.$playground.hide();
}
start()
{
this.hide();
this.add_listening_events();
}
update()
{
}
}
为了测试,接下来给这个player的运动,修改一下class Player
的代码的一些部分(这是为了测试,测试完之后可以删掉)。
constructor(...)
{
...;
this.vx = 1;
this.vy = 1;
}
update()
{
this.x += this.vx;
this.y += this.vy;
...
}
进行调试,发现这个球会移动,而且移动的时候尾部会出现模糊,这是因为GameMap
画上的颜色是透明的黑色蒙版,会带来这种视觉效果(可以模拟制作动画的时候,每一层画都加上黑色的透明蒙版,就会出现逐渐消失的原像,这就是模糊的感觉的由来)。
接下来给class Player
加入监听(鼠标操作和键盘操作等等),修改如下。
add_listening_events()
{
let outer = this; // 设置正确的this指针,因为接下来的后面的function内的this不是对象本身的this
this.playground.game_map.$canvas.on("contextmenu", function(){ // 关闭画布上的鼠标监听右键
return false;
});
this.playground.game_map.$canvas.mousedown(function(e){ // 鼠标监听
if (!outer.is_alive) return false; // 去世之后就不能动了
let ee = e.which; // e.which就是点击的键对应的值
if (ee === 3) // 右键
{
outer.move_to(e.clientX, e.clientY); // e.clientX是鼠标的x坐标,e.clientY同理
}
});
}
move_to(tx, ty)
{
console.log("move_to", tx, ty); // 测试输出
}
start()
{
if (this.is_me) // 只有这个玩家是自己的时候才能加入监听
{
this.add_listening_events();
}
}
接下来测试,在画布上随便用右键点击,会发现console
(网页控制台)里面会输出鼠标的位置。
为了后面的准备,我们首先定义一个全局函数GET_DIST(x1, y1, x2, y2)
来获取两点之间的距离,如下。
let GET_DIST = function(x1, y1, x2, y2)
{
let dx = x1 - x2, dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
接下来我们再进行对class Player
的修改。如下。
move_to(tx, ty)
{
this.move_length = GET_DIST(this.x, this.y, tx, ty); // 跟目的地的距离
let dx = tx - this.x, dy = ty - this.y;
let angle = Math.atan2(dy, dx); // 计算角度,这里Math.atan2(y, x)相当于求arctan(y / x);
this.vx = Math.cos(angle); // vx是这个速度(单位向量)的x上的速度(学过向量的都明白)
this.vy = Math.sin(angle); // vy是这个速度的y上的速度
}
update()
{
this.update_move(); // 更新移动
...
}
update_move() // 将移动单独写为一个过程
{
if (this.move_length < EPS) // 移动距离没了(小于精度)
{
this.move_length = 0; // 全都停下了
this.vx = this.vy = 0;
}
else // 否则继续移动
{
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // 每个时间微分里该走的距离
// 注意:this.timedelta 的单位是毫秒,所以要 / 1000 转换单位为秒
this.x += this.vx * moved; // 移动
this.y += this.vy * moved; // 移动
}
}
然后进行测试,成功。
接下来写发射火球。
首先实现一个火球类,创建game/static/js/src/playground/skill/fireball/zbase.js
,代码如下。
zbase.js
class Fireball extends AcGameObject
{
constructor(playground, player, x, y, radius, color, damage, vx, vy, speed, move_dist)
{
// 有些步骤前面重复过,这里不再赘述
super(true);
this.playground = playground;
this.player = player;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.radius = radius; // 半径
this.color = color;
this.damage = damage; // 伤害值
this.vx = vx; // 移动方向
this.vy = vy; // 移动方向
this.speed = speed; // 速度
this.move_dist = move_dist; // 射程
}
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();
}
start()
{
}
update()
{
this.update_move();
this.render();
}
update_move()
{
if (this.move_dist < EPS) // 如果走完射程了就消失
{
this.destroy();
return false;
}
let moved = Math.min(this.move_dist, this.speed * this.timedelta / 1000);
this.x += this.vx * moved;
this.y += this.vy * moved;
this.move_dist -= moved;
}
}
接下来实现如何发射这个火球。
首先在class Player
,改动一下。如下。
constructor(...)
{
...
this.cur_skill = null; // 当前选中的技能
...
}
add_listening_events()
{
...
this.playground.game_map.$canvas.mousedown(function(e){
...
else if (ee === 1)
{
if (outer.cur_skill === "fireball") // 当前技能是火球就发射
{
outer.shoot_fireball(e.clientX, e.clientY);
return false;
}
outer.cur_skill = null; // 点击之后就得清空
}
});
...
$(window).keydown(function(e){
if (!outer.is_alive) return false;
let ee = e.which;
if (ee === 81) // Q的keycode是81,其他keycode可以自行查阅
{
outer.cur_skill = "fireball"; // 技能选为fireball
return false;
}
});
...
}
shoot_fireball(tx, ty)
{
console.log(tx, ty); // 测试用
// 以下部分在测试成功之后再写入
let x = this.x, y = this.y;
let radius = this.playground.height * 0.01; // 半径
let color = "orange"; // 颜色
let damage = 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 speed = this.playground.height * 0.5; // 速度
let move_dist = this.playground.height * 1; // 射程
new FireBall(this.playground, this, x, y, radius, color, damage, vx, vy, speed, move_dist);
}
然后进行测试,可以成功发射火球。
我们要搞单机模式,也就是要加入其他的敌人,这时候我们进入class AcGamePlayground
,如下。
constructor()
{
...
for (let i = 0; i < 5; ++ i)
{
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, GET_RANDOM_COLOR(), false, this.height * 0.15));
}
}
这里获取随机颜色GET_RANDOM_COLOR()
可以用Math.random()
(返回0~1之间的实数)自行实现,或者可以用其他颜色。然后进游戏就可以发现生成了其他Player
,只是一动不动。所以接下来我们实现AI。
这里给出笔者的GET_RANDOM_COLOR()
的写法。(注释部分是另一种写法,可以尝试一下,笔者用的时候觉得有bug所以不用这样写的。)
let HEX = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
let GET_RANDOM_COLOR = function(){
let color = "#";
for (let i = 0; i < 6; ++ i)
{
color += HEX[Math.floor(Math.random() * 16)];
}
// let num = Math.floor(255 * 255 * 255 * Math.random());
// color += num.toString(16);
return color;
}
进入class Player
,修改如下。
update()
{
this.update_AI();
...
}
update_AI()
{
if (this.is_me) return false; // 如果这不是一个机器人就直接退出
this.update_AI_move();
}
update_AI_move()
{
if (this.move_length < EPS) // 如果停下来就随机选个地方走向那边
{
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
}
接下来实现火球的碰撞。有碰撞才能有实质性效果。我们这里将碰撞看作两边都各受对方碰撞 ,首先实现碰撞的逻辑,进入class FireBall
。修改如下。
let IS_COLLISION = function(obj1, obj2) // 这是一个全局函数,代表两个物体之间是否碰撞
{
return GET_DIST(obj1.x, obj1.y, obj2.x, obj2.y) < obj1.radius + obj2.radius; // 很简单的两圆相交条件
}
is_satisfy_collision(obj) // 真的碰撞的条件
{
if (this === obj) return false; // 自身不会被攻击
if (this.player === obj) return false; // 发射源不会被攻击
return IS_COLLISION(this, obj); // 距离是否满足
}
hit(obj) // 碰撞
{
obj.is_attacked(this); // obj被this攻击了
this.is_attacked(obj); // this被obj攻击了
}
is_attacked(obj) // 被伤害
{
this.is_attacked_concrete(0, 0); // 具体被伤害多少,火球不需要关注伤害值和血量,因为碰到后就直接消失
}
is_attacked_concrete(angle, damage) // 具体被伤害
{
this.destroy(); // 直接消失
}
update()
{
this.update_attack();
...
}
update_attack()
{
for (let i = 0; i < AC_GAME_OBJECTS.length; ++ i)
{
let obj = AC_GAME_OBJECTS[i];
if (this.is_satisfy_collision(obj)) // 如果真的碰撞了(这样可以保证碰撞条件可以自行定义,以后会很好维护)
{
this.hit(this, obj); // 两个物体碰撞了
break; // 火球,只能碰到一个物体
}
}
}
我们还要给class Player
加上被攻击的表现is_attacked()
。进入class Player
,修改如下。
is_attacked(obj)
{
let angle = Math.atan2(this.y - obj.y, this.x - obj.x); // 角度
let damage = obj.damage; // 伤害
// 注意,这里被伤害之后的表现,就是什么方向碰撞就是什么伤害,简单的向量方向计算
this.is_attacked_concrete(angle, damage);
}
is_attacked_concrete(angle, damage) // 被具体伤害
{
this.radius -= damage; // 这里半径就是血量
this.friction_damage = 0.8; // 击退移动摩擦力
if (this.is_died()) return false; // 已经去世了吗
this.x_damage = Math.cos(angle);
this.y_damage = Math.sin(angle); // (x_damage, y_damage)是伤害向量的方向向量
this.speed_damage = damage * 100; // 击退速度
}
is_died()
{
if (this.radius < EPS * 10) // 少于这个数表示已经去世
{
this.destroy(); // 去世
return true;
}
return false;
}
update_move()
{
if (this.speed_damage && this.speed_damage > EPS) // 如果此时在被击退的状态,就不能自己动
{
this.vx = this.vy = 0; // 不能自己动
this.move_length = 0; // 不能自己动
this.x += this.x_damage * this.speed_damage * this.timedelta / 1000; // 被击退的移动
this.y += this.y_damage * this.speed_damage * this.timedelta / 1000; // 被击退的移动
this.speed_damage *= this.friction_damage; // 摩擦力,表现出一个被击退越来越慢的效果
}
...
}
“游戏好不好玩看你参数调的好不好。” ——yxc
为了让碰撞看起来比较有打击感,我们可以加一个被伤害之后就喷射一些小粒子的效果。首先定义粒子类,创建game/static/js/src/playground/animation/particle/zbase.js
,然后代码如下。
zbase.js
// 这里面很多过程都是前面写过的,借这个机会努力回想一下。
class Particle extends AcGameObject
{
constructor(playground, x, y, radius, color, vx, vy, speed)
{
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.vx = vx;
this.vy = vy;
this.speed = speed;
}
render()
{
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.fillStyle = this.color;
this.fill();
}
start()
{
this.friction_speed = 0.8;
this.friction_radius = 0.8;
}
update()
{
this.update_move();
this.render();
}
update_move()
{
if (this.speed < EPS * 10 || this.radius < EPS * 10)
{
this.destroy();
return false;
}
this.x += this.vx * this.speed * this.timedelta / 1000;
this.y += this.vy * this.speed * this.timedelta / 1000;
this.speed *= this.friction_speed;
this.radius *= this.friction_radius;
}
}
然后我们再进入class Player
,修改代码如下。
is_attacked_concrete(angle, damage)
{
this.explode_particle(); // 爆发粒子
...
}
explode_particle()
{
for (let i = 0; i < 10 + Math.random() * 5; ++ i) // 粒子数
{
let x = this.x, y = this.y;
let radius = this.radius / 3;
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;
new Particle(this.playground, x, y, radius, color, vx, vy, speed); // 创建粒子对象
}
}
然后再尝试一下,效果很理想。
到了这里,可以说几乎完成了今天的目标。现在我们还差给AI赋予发射火球的权力。进入class Player
,修改如下。
start()
{
...
this.cold_time = 5; // 冷静期,开局5秒不能发动攻击
}
update_AI()
{
...
if (!this.update_AI_cold_time()) return false; // 还没走完冷静期,就不能放技能
this.update_AI_shoot_fireball(); // 发射火球
}
update_AI_cold_time() // 冷静期
{
if (this.cold_time > 0) // 如果处于冷静期,就不能放技能,返回false
{
this.cold_time -= this.timedelta / 1000; // 冷静期流逝
return false;
}
return true; // 过了冷静期,可以放技能了,返回true
}
update_AI_shoot_fireball()
{
if (Math.random() < 1 / 300.0) // 每隔一定时间发射一次
{
let player = this.playground.players[0]; // 这个可以设置为随机,自行实现
this.shoot_fireball(player.x, player.y); // 发射火球
}
}
至此,本课告一段落。
笔者有一种代码整理的方式,仅供参考。(树形结构)
class Object
{
constructor(静态相关的变量, 动态相关的变量)
{
}
add_listening_events();
render();
... // 各种功能过程
start()
{
this.start_1();
this.start_2();
}
start_1()
{
}
start_2()
{
}
update()
{
this.update_1();
this.update_2();
}
update_1()
{
}
update_2()
{
}
on_destroy()
{
this.on_destroy_1();
this.on_destroy_2();
}
on_destroy_1()
{
}
on_destroy_2()
{
}
}
现进度的成品代码
let AC_GAME_OBJECTS = []; // 储存所有可以“动”的元素的全局数组
class AcGameObject
{
constructor(hurtable = false) // 构造函数
{
AC_GAME_OBJECTS.push(this); // 将这个对象加入到储存动元素的全局数组里
this.has_call_start = false; // 记录这个对象是否已经调用了start函数
this.timedelta = 0; // 当前距离上一帧的时间间隔,相等于时间微分,用来防止因为不同浏览器不同的帧数,物体移动若按帧算会不同,所以要用统一的标准,就要用时间来衡量
this.hurtable = hurtable; // 决定这个元素能否被碰撞,默认为不能
}
start()
{
// 只会在第一帧执行一次的过程
}
update()
{
// 每一帧都会执行的过程
}
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); // 从数组中删除元素的函数splice()
break;
}
}
}
on_destroy()
{
// 被删除之前的过程,“临终遗言”
}
}
let last_timestp; // 上一帧的时间
let AC_GAME_ANIMATION = function(timestp) // timestp 是传入的一个参数,就是当前调用的时间
{
for (let i = 0; i < AC_GAME_OBJECTS.length; ++ i) // 所有动的元素都进行更新。
{
let obj = AC_GAME_OBJECTS[i];
if (!obj.has_called_start)
{
obj.start(); // 调用start()
obj.has_called_start = true; // 表示已经调用过start()了
}
else
{
obj.timedelta = timestp - last_timestp; // 时间微分
obj.update(); // 不断调用
}
}
last_timestp = timestp; // 进入下一帧时当前时间戳就是这一帧的时间戳
requestAnimationFrame(AC_GAME_ANIMATION); // 不断递归调用
}
requestAnimationFrame(AC_GAME_ANIMATION); // JS的API,可以调用1帧里面的函数。(有些浏览器的一秒帧数不一定相等)
class AcGamePlayground
{
constructor(root)
{
this.root = root;
this.$playground = $(`
<div class="ac-game-playground"></div>
`); // 这里定义了新HTML类
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", true, this.height * 0.15)); // 创建一个自己操控的玩家
for (let i = 0; i < 5; ++ i)
{
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, GET_RANDOM_COLOR(), false, this.height * 0.15)); // 创建其他机器人
}
this.$back = this.$playground.find('.ac-game-playground-item-back');
this.start();
}
add_listening_events()
{
let outer = this;
this.$back.click(function(){
outer.hide();
outer.root.$menu.show();
});
}
show()
{
this.$playground.show();
}
hide()
{
this.$playground.hide();
}
start()
{
this.hide();
this.add_listening_events();
}
update()
{
}
}
class GameMap extends AcGameObject
{
constructor(playground)
{
super(); // 调用基类的构造函数
this.playground = playground; // 这个Map是属于这个playground的
this.$canvas = $(`<canvas></canvas>`); // canvas是画布
this.ctx = this.$canvas[0].getContext('2d'); // 用ctx操作画布canvas
this.ctx.canvas.width = this.playground.width; // 设置画布的宽度
this.ctx.canvas.height = this.playground.height; // 设置画布的高度
this.playground.$playground.append(this.$canvas); // 将这个画布加入到这个playground
}
render()
{
this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)"; // 填充颜色设置为透明的黑色
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); // 画上给定的坐标的矩形
}
start()
{
}
update()
{
this.render(); // 这个地图要一直画一直画(动画的基本原理)
}
}
class Player extends AcGameObject
{
constructor(playground, x, y, radius, color, is_me, speed)
{
super(true);
this.playground = playground; // 所属playground
this.ctx = this.playground.game_map.ctx; // 操作的画笔
this.x = x; // 坐标
this.y = y; // 坐标
this.radius = radius; // 半径
this.color = color; // 颜色
this.is_me = is_me; // 玩家类型
this.speed = speed; // 速度
this.is_alive = true; // 是否存活
this.eps = 0.1; // 精度,这里建议定义为全局变量,EPS = 0.1,在这个教程里以后都这么用。
this.cur_skill = null; // 当前选中的技能
}
add_listening_events()
{
let outer = this; // 设置正确的this指针,因为接下来的后面的function内的this不是对象本身的this
this.playground.game_map.$canvas.on("contextmenu", function(){ // 关闭画布上的鼠标监听右键
return false;
});
this.playground.game_map.$canvas.mousedown(function(e){ // 鼠标监听
if (!this.is_alive) return false;
let ee = e.which; // e.which就是点击的键对应的值
if (ee === 3) // 右键
{
outer.move_to(e.clientX, e.clientY); // e.clientX是鼠标的x坐标,e.clientY同理
}
else if (ee === 1)
{
if (outer.cur_skill === "fireball")
{
outer.shoot_fireball(e.clientX, e.clientY);
return false;
}
outer.cur_skill = null; // 点击之后就得清空
}
});
$(window).keydown(function(e){
if (!this.is_alive) return false;
let ee = e.which;
if (ee === 81) // Q的keycode是81,其他keycode可以自行查阅
{
outer.cur_skill = "fireball"; // 技能选为fireball
return false;
}
});
}
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();
}
move_to(tx, ty)
{
this.move_length = GET_DIST(this.x, this.y, tx, ty); // 跟目的地的距离
let dx = tx - this.x, dy = ty - this.y;
let angle = Math.atan2(dy, dx); // 计算角度,这里Math.atan2(y, x)相当于求arctan(y / x);
this.vx = Math.cos(angle); // vx是这个速度(单位向量)的x上的速度(学过向量的都明白)
this.vy = Math.sin(angle); // vy是这个速度的y上的速度
}
shoot_fireball(tx, ty)
{
console.log(tx, ty); // 测试用
// 以下部分在测试成功之后再写入
let x = this.x, y = this.y;
let radius = this.playground.height * 0.01; // 半径
let color = "orange"; // 颜色
let damage = 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 speed = this.playground.height * 0.5; // 速度
let move_dist = this.playground.height * 1; // 射程
new FireBall(this.playground, this, x, y, radius, color, damage, vx, vy, speed, move_dist);
}
is_attacked(obj)
{
let angle = Math.atan2(this.y - obj.y, this.x - obj.x); // 角度
let damage = obj.damage; // 伤害
// 注意,这里被伤害之后的表现,就是什么方向碰撞就是什么伤害,简单的向量方向计算
this.is_attacked_concrete(angle, damage);
}
is_attacked_concrete(angle, damage) // 被具体伤害
{
this.explode_particle();
this.radius -= damage; // 这里半径就是血量
this.friction_damage = 0.8; // 击退移动摩擦力
if (this.is_died()) return false; // 已经去世了吗
this.x_damage = Math.cos(angle);
this.y_damage = Math.sin(angle); // (x_damage, y_damage)是伤害向量的方向向量
this.speed_damage = damage * 100; // 击退速度
}
explode_particle()
{
for (let i = 0; i < 10 + Math.random() * 5; ++ i) // 粒子数
{
let x = this.x, y = this.y;
let radius = this.radius / 3;
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;
new Particle(this.playground, x, y, radius, color, vx, vy, speed); // 创建粒子对象
}
}
is_died()
{
if (this.radius < EPS * 10) // 少于这个数表示已经去世
{
this.destroy(); //消失
return true;
}
return false;
}
start()
{
this.start_add_listening_events();
this.cold_time = 5;
}
start_add_listening_evnet()
{
if (this.is_me)
{
this.add_listening_evnets();
}
}
update()
{
this.update_AI();
this.update_move(); // 更新移动
this.render(); // 同样要一直画一直画(yxc:“人不吃饭会死,物体不一直画会消失。”)
}
update_AI()
{
if (this.is_me) return false; // 如果这不是一个机器人就直接退出
this.update_AI_move();
if (!this.update_AI_cold_time()) return false; // 还没走完冷静期,就不能放技能
this.update_AI_shoot_fireball(); // 发射火球
}
update_AI_move()
{
if (this.move_length < EPS) // 如果停下来就随机选个地方走向那边
{
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
}
update_AI_cold_time() // 冷静期
{
if (this.cold_time > 0) // 如果处于冷静期,就不能放技能,返回false
{
this.cold_time -= this.timedelta / 1000; // 冷静期流逝
return false;
}
return true; // 过了冷静期,可以放技能了,返回true
}
update_AI_shoot_fireball()
{
if (Math.random() < 1 / 300.0) // 每隔一定时间发射一次
{
let player = this.playground.players[0]; // 这个可以设置为随机,自行实现
this.shoot_fireball(player.x, player.y); // 发射火球
}
}
update_move() // 将移动单独写为一个过程
{
if (this.speed_damage && this.speed_damage > EPS) // 如果此时在被击退的状态,就不能自己动
{
this.vx = this.vy = 0; // 不能自己动
this.move_length = 0; // 不能自己动
this.x += this.x_damage * this.speed_damage * this.timedelta / 1000; // 被击退的移动
this.y += this.y_damage * this.speed_damage * this.timedelta / 1000; // 被击退的移动
this.speed_damage *= this.friction_damage; // 摩擦力,表现出一个被击退越来越慢的效果
}
if (this.move_length < EPS) // 移动距离没了(小于精度)
{
this.move_length = 0; // 全都停下了
this.vx = this.vy = 0;
}
else // 否则继续移动
{
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // 每个时间微分里该走的距离
// 注意:this.timedelta 的单位是毫秒,所以要 / 1000 转换单位为秒
this.x += this.vx * moved; // 移动
this.y += this.vy * moved; // 移动
}
}
on_destroy() // 死之前在this.players数组里面删掉这个player
{
this.is_alive = false;
for (let i = 0; i < this.playground.players.length; ++ i)
{
let player = this.playground.players[i];
if (this === player)
{
this.playground.players.splice(i, 1);
}
}
}
}
class Fireball extends AcGameObject
{
constructor(playground, player, x, y, radius, color, damage, vx, vy, speed, move_dist)
{
// 有些步骤前面重复过,这里不再赘述
super(true);
this.playground = playground;
this.player = player;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.radius = radius; // 半径
this.color = color;
this.damage = damage; // 伤害值
this.vx = vx; // 移动方向
this.vy = vy; // 移动方向
this.speed = speed; // 速度
this.move_dist = move_dist; // 射程
}
is_satisfy_collision(obj) // 真的碰撞的条件
{
if (this === obj) return false; // 自身不会被攻击
if (this.player === obj) return false; // 发射源不会被攻击
return IS_COLLISION(this, obj); // 距离是否满足
}
hit(obj) // 碰撞
{
obj.is_attacked(this); // obj被this攻击了
this.is_attacked(obj); // this被obj攻击了
}
is_attacked(obj) // 被伤害
{
this.is_attacked_concrete(0, 0); // 具体被伤害多少,火球不需要关注伤害值和血量,因为碰到后就直接消失
}
is_attacked_concrete(angle, 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();
}
start()
{
}
update()
{
this.update_attack();
this.update_move();
this.render();
}
update_attack()
{
for (let i = 0; i < AC_GAME_OBJECTS.length; ++ i)
{
let obj = AC_GAME_OBJECTS[i];
if (this.is_satisfy_collision(obj)) // 如果真的碰撞了(这样可以保证碰撞条件可以自行定义,以后会很好维护)
{
this.hit(this, obj); // 两个物体碰撞了
break; // 火球,只能碰到一个物体
}
}
}
update_move()
{
if (this.move_dist < EPS) // 如果走完射程了就消失
{
this.destroy();
return false;
}
let moved = Math.min(this.move_dist, this.speed * this.timedelta / 1000);
this.x += this.vx * moved;
this.y += this.vy * moved;
this.move_dist -= moved;
}
}
class Particle extends AcGameObject
{
constructor(playground, x, y, radius, color, vx, vy, speed)
{
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.vx = vx;
this.vy = vy;
this.speed = speed;
}
render()
{
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.fillStyle = this.color;
this.fill();
}
start()
{
this.friction_speed = 0.8;
this.friction_radius = 0.8;
}
update()
{
this.update_move();
this.render();
}
update_move()
{
if (this.speed < EPS * 10 || this.radius < EPS * 10)
{
this.destroy();
return false;
}
this.x += this.vx * this.speed * this.timedelta / 1000;
this.y += this.vy * this.speed * this.timedelta / 1000;
this.speed *= this.friction_speed;
this.radius *= this.friction_radius;
}
}
加一行代码 不然小球不会停下来:
看到您把GET_DIST、IS_COLLISION都归纳成全局函数了,是要在哪个文件里定义他们,AcGameObject吗
在
Particle
类里面再加上一个is_attackde()
函数不然当火球与粒子发生碰撞时会报错
为什么直接复制y总的代码,playground页面没有显示
在
Particle
里面加上ctx
:FireBall
里面要改一下:bug修改:
hit(this, obj) 应该是 hit(obj)
太强了
···
if (outer.cur_skill === “fireball”)
{
outer.shoot_fireball(e.clientX, e.clientY);
outer.cur_skill = null ;
return false;
}
outer.cur_skill = null; // 点击之后就得清空
···
这里在后边赋值就更新不了了,当然这样写的话,就是开了Q就是无线放了
为什么监听函数可以一直监听,他不是在start函数里面吗,没弄明白,大佬可以解答一下吗
相当于添加监听器,所以可以一直监听,不是一直执行才能监听,是加了监听器就一直监听。
就是在start()中执行后开始监听,一旦点击鼠标右键就执行一边mousedown()是吗?
是的.
棒
niu
优质小作文
写的不错 支持一下
%%%%%%