文章目录
- 游戏界面实现
- 修改js模块化
- 增加界面样式
- 实现简易游戏引擎
- 创建人物
- 创建火球类
- 实现碰撞
- 实现动态效果
游戏界面实现
修改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; anvas37 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"; // 技能选为fireballreturn 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; // 所属playgroundthis.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不是对象本身的thisthis.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"; // 技能选为fireballreturn 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);}}}
}
大致碰撞效果如上。