游戏界面实现
修改js模块化
在game/templates/multiends/web.html
并且删除<head></head>
里面的AcGame
在game/static/js/src/zbase.js
里
在class
前加上export
即可完成,再重新打包
增加界面样式
在game/static/css/game.css
.ac-game-playground{
width:100%;
height:100%;
user-select:none;
}
在game/static/js/src/playground中先存下界面的长和宽
this.width=this.$playground.width(); this.weight=this.$playground.height();
实现简易游戏引擎
在game/static/js/src/playground创建ac_game_object文件夹,并在文件夹内创建zbase.js
let AC_GAME_OBJECTS=[];
class AcGameObject{
constructor(){
AC_GAME_OBJECTS.push(this);
this.has_called_start=false; //是否执行过start函数
this.timedelta=0; //当前帧距离上一帧的时间间隔
}
start(){ //只会在第一帧执行
}
update(){ //每一帧会执行一次
}
on_destory(){ //被删除之前执行一次
}
destory(){ //删掉该物体
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);
break;
}
}
}
}
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);
在game/static/js/src/playground/创建game_map文件夹,并在文件夹内创建zbase.js
class GameMap extends AcGameObject{
2 constructor(playground){
3 super();
4 this.playground =playground;
5 this.$canvas=$(`<canvas></canvas>`);
6 this.ctx=this.$canvas[0].getContext('2d');
7 this.ctx.canvas.width=this.playground.width;
8 this.ctx.canvas.height=this.playground.height;
9 this.playground.$playground.append(this.$canvas);
10 }
11
12 start(){
13 }
14
15 update(){
16 this.render();
17 }
18
19 render(){
20 this.ctx.fillStyle="rgba(0,0,0,0.2)";
21 this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);
22 }
23 }
创建人物
class Player extends AcGameObject{
2 constructor(playground,x,y,radius,color,speed,is_me){
3 super();
4 this.playground=playground;
5 this.ctx=this.playground.game_map.ctx;
6 this.x=x;
7 this.y=y;
this.vx=0;
this.vy=0;
this.move_length=0;
8 this.radius=radius;
9 this.color=color;
10 this.speed=speed;
11 this.is_me=is_me;
12 this.eps=0.1;
13 }
16
17 start(){
18 if(this.is_me){
19 this.add_listening_events();
20 }
21 }
22 add_listening_events(){
23 let outer=this;
24 this.playground.game_map.$canvas.on("contextmenu",function(){
25 return false;
26 });
27 this.playground.game_map.$canvas.mousedown(function(e){
28 if(e.which === 3){
29 outer.move_to(e.clientX,e.clientY);
30 }
31 });
32 }
33
34 get_dist(x1,y1,x2,y2){
35 let dx=x1-x2;
36 let dy=y1-y2; anvas
37 return Math.sqrt(dx*dx+dy*dy);
38
39 }
40
41
42 move_to(tx,ty){
43 console.log("move to",this.x,this.y,tx,t
y);
44 this.move_length=this.get_dist(this.x,th
is.y,tx,ty);
45 let angle=Math.atan2(ty-this.y,tx-this.x);
46 this.vx=Math.cos(angle);
47 this.vy=Math.sin(angle);
48 console.log("angle",this.move_length,this.vx,this.vy);
49 }
50
51 update(){
52 if(this.move_length<this.eps){
53 this.move_length=0;
54 this.vx=this.vy=0;
55 }else{
56 let moved=Math.min(this.move_length,this.speed*this.timedelta/1000);
57 //console.log(this.angle,this.move_length,this.speed,this.timedelta/1000);
58 this.x+=this.vx*moved;
59 this.y+=this.vy*moved;
60 this.move_length-=moved;
61
62 }
63 this.render();
64 }
22 render(){
23 this.ctx.beginPath();
24 this.ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,false);
25 this.ctx.fillStyle=this.color;
26 this.ctx.fill();
在AcGamePlayground中加入:
创建火球类
1 class FireBall extends AcGameObject{
2 constructor(playground,player,x,y,radius,vx,vy,color,speed,move_length){
3 super();
4 this.playground=playground;
5 this.player=player;
6 this.ctx=this.playground.game_map.ctx;
7 this.x=x;
8 this.y=y;
9 this.vx=vx;
10 this.vy=vy;
11 this.color=color;
12 this.speed=speed;
13 this.move_length=move_length;
14 this.eps=0.1;
15 }
16 start(){
17 }
18 update(){
19 if(this.move_length<this.eps){
20 this.destroy();
21 return false;
22 }
23 let moved=Math.min(this.move_length,this.speed*this.timedelta/1000);
24 this.x+=this.vx*moved;
25 this.y+=this.vy*moved;
26 this.move_length-=moved;
27
28 this.render();
29
30 }
31
32 render(){
33 this.ctx.beginPath();
34 this.ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,false);
35 this.ctx.fillStyle=this.color;
36 this.ctx.fill();
37 }
38 }
在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);
随机生成其他敌人
constructor()
{
...
for (let i = 0; i < 5; ++ i)//随机生成5个敌人
{
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, GET_RANDOM_COLOR(), false, this.height * 0.15));
}
}
修改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);
}
}
实现碰撞
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; // 火球,只能碰到一个物体
}
}
}
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; // 摩擦力,表现出一个被击退越来越慢的效果
}
...
}
实现动态效果
// 这里面很多过程都是前面写过的,借这个机会努力回想一下。
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;
}
}
修改后的Player:
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);
}
}
}
}
大致碰撞效果如上。