写在前面
- 前排提醒:本篇笔记没有记录实现冷却技能和闪现技能的代码。因为基本是前端代码,通信逻辑和移动、发炮的逻辑是雷同的,其次本人对游戏技能不太感兴趣,再者火炮连发多爽,干嘛加个冷却时间。
前摇
- 删除console.log函数——
ag console.log
- 完善acapp中的退出功能
//game/static/js/src/settings/zbase.js
logout_on_remote() { // 在远程服务器上登出
if (this.platform === "ACAPP") {
this.root.AcWingOS.api.window.close();
} else {
$.ajax({
url: "https://app165.acapp.acwing.com.cn/settings/logout/",
type: "GET",
success: function(resp) {
if (resp.result === "success") {
location.reload();
}
}
});
}
}
对需要广播的函数进行广播
基本逻辑
- 前端——写自定义send函数和自定义receive函数,负责与服务器的信息交互
- 后端——写对应的自定义receive函数
- 无论前端还是后端都要再写一下路由
- 什么路由——意思就是,在前后端信息交互的时候真正负责通信的是框架提供的api函数,而我们为了代码可读性更高所以会根据接收的信息标志而用不同的自定义函数去处理信息,但是框架又不晓得我们的自定义函数,所以我们就会在api函数中调用我们写的自定义函数。这就是写路由。
移动函数(move_to函数)
- send_move_to函数——向服务器发送本地玩家移动信息
//game/static/js/src/playground/socket/multiplayer/zbase.js
send_move_to(tx, ty) {
let outer = this;
this.ws.send(JSON.stringify({
'event': "move_to",
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
}));
}
- receive_move_to函数——接收服务器广播的同房间玩家的移动信息
receive_move_to(uuid, tx, ty) {
let player = this.get_player(uuid);
if (player) {
player.move_to(tx, ty);
}
}
- get_player函数——通过uuid来获得对应的玩家类
get_player(uuid) {
let players = this.playground.players;
fort i = 0; i < players.length; i ++ ) {
let player = players[i];
if (player.uuid === uuid)
return player;
}
return null;
}
- 后端部分
- 在此之前把group_create_player函数名修改为group_send_event,把create_player函数中的type改为group_send_event
- 这里要提一下这个channel_lay.group_send函数中的type什么意思
- 我本来以为这是个回调函数,用来处理后续前端的信息,但是不对
- channel_lay.group_send其实并不负责发送消息,他负责向组内链接广播信息,那么这些链接用什么向前端发送消息,就是type指明的函数
#game/consumers/multiplayer/index.py
async def move_to(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_send_event",
'event': "move_to",
'uuid': data['uuid'],
'tx': data['tx'],
'ty': data['ty'],
}
)
- 添加路由(代码会在最后贴)
- 在player的移动函数中调用我们写好的通信函数
//game/static/js/src/playground/player/zbase.js
if (e.which === 3) {
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty);
if (outer.playground.mode === "multi mode") {
outer.playground.mps.send_move_to(tx, ty);
}
攻击函数
- 先将每个人发的子弹存下来(每个子弹都有uuid)——用数组储存
//game/static/js/src/playground/player/zbase.js
//添加一个数组
this.fireballs = [];
//把火球放到数组里面
shoot_fireball(tx, ty) {
let x = this.x, y = this.y;
let radius = 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 = 0.5;
let move_length = 1;
let fireball = new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, 0.01);
this.fireballs.push(fireball);
return fireball;
}
- 编写前端关于send和receive函数
//game/static/js/src/playground/socket/multiplayer/zbase.js
send_shoot_fireball(tx, ty, ball_uuid) {
let outer = this;
this.ws.send(JSON.stringify({
'event': "shoot_fireball",
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
'ball_uuid': ball_uuid,
}));
}
receive_shoot_fireball(uuid, tx, ty, ball_uuid) {
let player = this.get_player(uuid);
if (player) {
let fireball = player.shoot_fireball(tx, ty);
fireball.uuid = ball_uuid;
}
}
- 删除火球的函数,在player和fireball里都要实现(这里存在疑问,等实际敲的时候看一下)
- 为什么要删两个:因为js里的对象只有把它所有的引用都删掉才会真正删掉
- 这个on_destroy函数在destroy函数中被调用——这是在父类中写好的
- 当外界调用destroy_fireball函数后,首先调用on_destroy函数负责在player的火球数组中删去火球,然后在
AC_GAME_OBJECTS
中删去火球
- 当外界调用destroy_fireball函数后,首先调用on_destroy函数负责在player的火球数组中删去火球,然后在
//game/static/js/src/playground/player/zbase.js
destroy_fireball(uuid) {
for (let i = 0; i < this.fireballs.length; i ++ ) {
let fireball = this.fireballs[i];
if (fireball.uuid === uuid) {
fireball.destroy();
break;
}
}
}
//game/static/js/src/playground/skill/fireball/zbase.js
on_destroy() {
let fireballs = this.player.fireballs;
for (let i = 0; i < fireballs.length; i ++ ) {
if (fireballs[i] === this) {
fireballs.splice(i, 1);
break;
}
}
- fireball的修改update函数——这里是把原来的update函数拆成了两个函数
update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}
this.update_move();
if (this.player.character !== "enemy") {
this.update_attack();
}
this.render();
}
update_move(){
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;
}
update_attack(){
for(let i = 0; i<this.playground.players.length; i++){
let player = this.playground.player[i];
if(this.player !== player && this.is_collision(player)){
this.attack(player);
break;
}
}
- 后端
async def shoot_fireball(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_send_event",
'event': "shoot_fireball",
'uuid': data['uuid'],
'tx': data['tx'],
'ty': data['ty'],
'ball_uuid': data['ball_uuid'],
}
)
- player中调用
else if (e.which === 1) {
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
if (outer.cur_skill === "fireball") {
let fireball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === "multi mode") {
outer.playground.mps.send_shoot_fireball(tx, ty, fireball.uuid);
}
}
outer.cur_skill = null;
}
- 在写完上面这些代码后,出现了一个火球消失不了的bug。原因就是我们在player里面存储的火球数组中没有删掉火球。
调整一下分组逻辑
class MultiPlayer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
async def disconnect(self, close_code):
print('disconnect')
await self.channel_layer.group_discard(self.room_name, self.channel_name);
async def create_player(self, data): # 这里我们把分房间的代码放到create_player函数
self.room_name = None
for i in range(start, 100000000):
name = "room-%d" % (i)
if not cache.has_key(name) or len(cache.get(name)) < settings.ROOM_CAPACITY:
self.room_name = name
break
if not self.room_name:
return
碰撞判断
为了避免网速或者浮点数计算误差等原因导致的不同玩家之间的不同步,所以我们决定在炮弹碰撞检测的同时对是否击中玩家、击中玩家的位置、击中玩家弹射的角度等进行同步,而同步的标准由炮弹发送玩家进行广播。
- 前端
- 除了要编写multiplayer类中的send和receive函数
- 还要在player类中补充一个receive_attack函数
- 为什么不直接调用is_attack函数——因为我们需要同时同步被击中者的坐标,is_attack只负责angle和damage
- 还要在fireball类中attack函数中调用multiplayer类中的send函数
//game/static/js/src/playground/socket/multiplayer/zbase.js
send_attack(attackee_uuid, x, y, angle, damage, ball_uuid) { //被攻击者的uuid,受到攻击的坐标,角度,伤害,击中炮弹的uuid
let outer = this; //因为攻击发出者是自己,所以就不用再传一个攻击者的uuid
this.ws.send(JSON.stringify({
'event': "attack",
'uuid': outer.uuid,
'attackee_uuid': attackee_uuid,
'x': x,
'y': y,
'angle': angle,
'damage': damage,
'ball_uuid': ball_uuid,
}));
}
receive_attack(uuid, attackee_uuid, x, y, angle, damage, ball_uuid) {//攻击者的uuid,被攻击者的uid,受到攻击的坐标,角度,伤害,击中炮弹的uuid
let attacker = this.get_player(uuid);
let attackee = this.get_player(attackee_uuid);
if (attacker && attackee) {
attackee.receive_attack(x, y, angle, damage, ball_uuid, attacker);
}
}
//game/static/js/src/playground/player/zbase.js
receive_attack(x, y, angle, damage, ball_uuid, attacker) {
attacker.destroy_fireball(ball_uuid);
this.x = x;
this.y = y;
this.is_attacked(angle, damage);
}
// game/static/js/src/playground/skill/fireball/zbase.js
attack(player) {
let angle = Math.atan2(player.y - this.y, player.x - this.x);
player.is_attacked(angle, this.damage);
if (this.playground.mode === "multi mode") {
this.playground.mps.send_attack(player.uuid, player.x, player.y, angle, this.damage, this.uuid);
}
this.destroy();
}
- 后端
# game/consumers/multiplayer/index.py
async def attack(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_send_event",
'event': "attack",
'uuid': data['uuid'],
'attackee_uuid': data['attackee_uuid'],
'x': data['x'],
'y': data['y'],
'angle': data['angle'],
'damage': data['damage'],
'ball_uuid': data['ball_uuid'],
}
)
- 添加路由
计分牌功能
- 玩家状态——初始waiting,房间满3个人fighting,游戏结束over
//game/static/js/src/playground/notice_board/zbase.js
class NoticeBoard extends AcGameObject {
constructor(playground) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.text = "已就绪:0人";
}
start() {
}
write(text) {
this.text = text;
}
update() {
this.render();
}
render() { // canvas渲染文本
this.ctx.font = "20px serif";
this.ctx.fillStyle = "white";
this.ctx.textAlign = "center";
this.ctx.fillText(this.text, this.playground.width / 2, 20);
}
}
//game/static/js/src/playground/zbase.js
show(mode) { //
let outer = this;
this.$playground.show();
this.width = this.$playground.width();
this.height = this.$playground.height();
this.game_map = new GameMap(this);
this.mode = mode;
this.state = "waiting"; // waiting -> fighting -> over
this.notice_board = new NoticeBoard(this); // 在playground界面将计分牌渲染出来
this.player_count = 0;
this.resize();
//game/static/js/src/playground/player/zbase.js
start() {
this.playground.player_count ++ ; //当新建玩家就在计分牌处++
this.playground.notice_board.write("已就绪:" + this.playground.player_count + "人");
if (this.playground.player_count >= 3) {
this.playground.state = "fighting";
this.playground.notice_board.write("Fighting");
}
if (this.character === "me") {
- 添加只有在fighting状态才可以移动和发技能
//game/static/js/src/playground/player/zbase.js
//在监听函数处的移动和攻击功能处添加以下代码
if (outer.playground.state !== "fighting")
return false;
前后端路由
//前端 game/static/js/src/playground/socket/multiplayer/zbase.js
receive () {
let outer = this;
this.ws.onmessage = function(e) {
let data = JSON.parse(e.data);
let uuid = data.uuid;
if (uuid === outer.uuid) return false;
let event = data.event;
if (event === "create_player") {
outer.receive_create_player(uuid, data.username, data.photo);
} else if (event === "move_to") {
outer.receive_move_to(uuid, data.tx, data.ty);
} else if (event === "shoot_fireball") {
outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.ball_uuid);
} else if (event === "attack") {
outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.angle, data.damage, data.ball_uuid);
}
};
}
# game/consumers/multiplayer/index.py
async def receive(self, text_data):
data = json.loads(text_data)
event = data['event']
if event == "create_player":
await self.create_player(data)
elif event == "move_to":
await self.move_t
o(data)
elif event == "shoot_fireball":
await self.shoot_fireball(data)
elif event == "attack":
await self.attack(data)
太强了ORZ
很有帮助的总结
写的真好