Содержание примечаний воспроизведено из раздаточного материала курса AcWing Django Framework, ссылка на курс: Курс AcWing Django Framework .
СОДЕРЖАНИЕ
1. Напишите функцию синхронизации перемещения move_to.
Подобно функции синхронизации из предыдущей главы create_player
, синхронизацию мобильной функции также необходимо реализовать во внешнем интерфейсе send_move_to
и receive_move_to
самой функции. Модифицируем MultiPlayerSocket
класс (в каталоге ~/djangoapp/game/static/js/src/playground/socket/multiplayer
):
class MultiPlayerSocket {
constructor(playground) {
this.playground = playground;
// 直接将网站链接复制过来,将https改成wss,如果没有配置https那就改成ws,然后最后加上wss的路由
this.ws = new WebSocket('wss://app4007.acapp.acwing.com.cn/wss/multiplayer/');
this.start();
}
start() {
this.receive();
}
receive() {
let outer = this;
this.ws.onmessage = function(e) {
let data = JSON.parse(e.data); // 将字符串变回JSON
let uuid = data.uuid;
if (uuid === outer.uuid) return false; // 如果是给自己发送消息就直接过滤掉
let event = data.event;
if (event === 'create_player') {
// create_player路由
outer.receive_create_player(uuid, data.username, data.avatar);
} else if (event === 'move_to') {
// move_to路由
outer.receive_move_to(uuid, data.tx, data.ty);
}
};
}
send_create_player(username, avatar) {
...
}
receive_create_player(uuid, username, avatar) {
...
}
// 根据uuid找到对应的Player
get_player(uuid) {
let players = this.playground.players;
for (let i = 0; i < players.length; i++) {
let player = players[i];
if (player.uuid === uuid)
return player;
}
return null;
}
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(uuid, tx, ty) {
let player = this.get_player(uuid);
if (player) {
// 确保玩家存在再调用move_to函数
player.move_to(tx, ty);
}
}
}
Затем измените код внутренней связи ( файл ~/djangoapp/game/consumers/multiplayer
в каталоге ):index.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
from django.conf import settings
from django.core.cache import cache
class MultiPlayer(AsyncWebsocketConsumer):
async def connect(self):
...
async def disconnect(self, close_code):
...
async def create_player(self, data): # async表示异步函数
...
async def group_send_event(self, data): # 组内的每个连接接收到消息后直接发给前端即可
await self.send(text_data=json.dumps(data))
async def move_to(self, data): # 与create_player函数相似
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'],
}
)
async def receive(self, text_data):
data = json.loads(text_data)
print(data)
event = data['event']
if event == 'create_player': # 做一个路由
await self.create_player(data)
elif event == 'move_to': # move_to的路由
await self.move_to(data)
Наконец нам нужно вызвать функцию.Сначала нам нужно AcGamePlayground
записать режим игры в класс mode
:
class AcGamePlayground {
...
// 显示playground界面
show(mode) {
...
this.mode = mode; // 需要将模式记录下来,之后玩家在不同的模式中需要调用不同的函数
this.resize(); // 界面打开后需要resize一次,需要将game_map也resize
...
}
...
}
Затем Player
измените его в классе.Когда это многопользовательский режим, вам нужно транслировать move_to
сигнал:
class Player extends AcGameObject {
...
add_listening_events() {
let outer = this;
this.playground.game_map.$canvas.on('contextmenu', function() {
return false;
}); // 取消右键的菜单功能
this.playground.game_map.$canvas.mousedown(function(e) {
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {
// 1表示左键,2表示滚轮,3表示右键
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty); // e.clientX/Y为鼠标点击坐标
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_move_to(tx, ty);
}
} else if (e.which === 1) {
...
}
});
...
}
...
}
Теперь возможно одновременное перемещение нескольких игроков. Когда игрок в окне A перемещается, сначала Player
функция прослушивания окна (класса) будет управлять движением игрока, а затем определять, что это многопользовательский режим, поэтому вызывается функция MultiPlayerSocket
в классе для отправки информации на сервер (путем отправки событие), то функция на стороне сервера (в файле) получит информацию, и когда событие будет найдено , будет вызвана функция , которая отправит групповое сообщение всем остальным игрокам в этой комнате, и каждому Окно будет отображаться во внешнем интерфейсе ( в классе). Функция получает информацию и направляется к функции через событие, которое вызывает функцию каждого игрока через .send_move_to
WebSocket
~/djangoapp/game/consumers/multiplayer/index.py
receive
event
move_to
move_to
MultiPlayerSocket
receive
receive_move_to
uuid
move_to
2. Напишите функцию синхронизации атаки Shoot_fireball.
Поскольку выпущенные огненные шары исчезнут, огненные шары, выпущенные каждым игроком, необходимо сначала сохранить.Кроме того, мы реализуем функцию uuid
для удаления огненного шара в соответствии с огненным шаром и Player
модифицируем ее в классе:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
this.fire_balls = []; // 存下玩家发射的火球
...
}
...
// 向(tx, ty)位置发射火球
shoot_fireball(tx, ty) {
let x = this.x, y = this.y;
let radius = 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 = 0.5;
let move_length = 0.8;
let fire_ball = new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, 0.01);
this.fire_balls.push(fire_ball);
return fire_ball; // 返回fire_ball是为了获取自己创建这个火球的uuid
}
destroy_fireball(uuid) {
// 删除火球
for (let i = 0; i < this.fire_balls.length; i++) {
let fire_ball = this.fire_balls[i];
if (fire_ball.uuid === uuid) {
fire_ball.destroy();
break;
}
}
}
...
}
Поскольку у огненного шара Player
есть копия, нам нужно удалить ее, прежде чем удалять Player
огненный шар fire_balls
. А поскольку функции FireBall
в классе update
слишком раздуты, можно сначала разделить их update_move
на и update_attack
, модифицируем FireBall
класс:
class FireBall extends AcGameObject {
// 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
...
}
start() {
}
update_move() {
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;
}
update_attack() {
// 攻击碰撞检测
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攻击player
}
}
}
update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}
this.update_move();
this.update_attack();
this.render();
}
get_dist(x1, y1, x2, y2) {
...
}
is_collision(player) {
...
}
attack(player) {
...
}
render() {
...
}
on_destroy() {
let fire_balls = this.player.fire_balls;
for (let i = 0; i < fire_balls.length; i++) {
if (fire_balls[i] === this) {
fire_balls.splice(i, 1);
break;
}
}
}
}
Затем мы MultiPlayerSocket
реализуем функции send_shoot_fireball
и receive_shoot_fireball
в классе:
class MultiPlayerSocket {
...
receive() {
let outer = this;
this.ws.onmessage = function(e) {
let data = JSON.parse(e.data); // 将字符串变回JSON
let uuid = data.uuid;
if (uuid === outer.uuid) return false; // 如果是给自己发送消息就直接过滤掉
let event = data.event;
if (event === 'create_player') {
// create_player路由
outer.receive_create_player(uuid, data.username, data.avatar);
} else if (event === 'move_to') {
// move_to路由
outer.receive_move_to(uuid, data.tx, data.ty);
} else if (event === 'shoot_fireball') {
// shoot_fireball路由
outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.fireball_uuid);
}
};
}
...
send_shoot_fireball(tx, ty, fireball_uuid) {
let outer = this;
this.ws.send(JSON.stringify({
'event': 'shoot_fireball',
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
'fireball_uuid': fireball_uuid,
}));
}
receive_shoot_fireball(uuid, tx, ty, fireball_uuid) {
let player = this.get_player(uuid);
if (player) {
let fire_ball = player.shoot_fireball(tx, ty);
fire_ball.uuid = fireball_uuid; // 所有窗口同一个火球的uuid需要统一
}
}
}
Теперь нам нужно реализовать бэкэнд-функцию:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from django.core.cache import cache
class MultiPlayer(AsyncWebsocketConsumer):
...
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'],
'fireball_uuid': data['fireball_uuid'],
}
)
async def receive(self, text_data):
data = json.loads(text_data)
print(data)
event = data['event']
if event == 'create_player': # 做一个路由
await self.create_player(data)
elif event == 'move_to': # move_to的路由
await self.move_to(data)
elif event == 'shoot_fireball': # shoot_fireball的路由
await self.shoot_fireball(data)
Наконец, Player
функция вызывается в классе:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
}
start() {
...
}
add_listening_events() {
let outer = this;
this.playground.game_map.$canvas.on('contextmenu', function() {
return false;
}); // 取消右键的菜单功能
this.playground.game_map.$canvas.mousedown(function(e) {
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {
// 1表示左键,2表示滚轮,3表示右键
...
} 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 fire_ball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
}
}
outer.cur_skill = null; // 释放完一次技能后还原
}
});
$(window).keydown(function(e) {
if (e.which === 81) {
// Q键
outer.cur_skill = 'fireball';
return false;
}
});
}
// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
...
}
// 向(tx, ty)位置发射火球
shoot_fireball(tx, ty) {
...
}
destroy_fireball(uuid) {
// 删除火球
...
}
move_to(tx, ty) {
...
}
is_attacked(theta, damage) {
// 被攻击到
...
}
// 更新移动
update_move() {
...
}
update() {
...
}
render() {
...
}
on_destroy() {
for (let i = 0; i < this.playground.players.length; i++) {
if (this.playground.players[i] === this) {
this.playground.players.splice(i, 1);
break;
}
}
}
}
3. Напишите атаку функции синхронизации определения попадания.
Нам нужно унифицировать атакующее действие и использовать одно окно, чтобы однозначно определить попадание. Если оно попадет, оно будет транслироваться в другие окна. Поэтому огненные шары, запускаемые другими игроками в окне, являются лишь анимацией, и не должно быть никаких решимость ударить. Давайте сначала FireBall
внесем изменения в класс :
class FireBall extends AcGameObject {
...
update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}
this.update_move();
if (this.player.character !== 'enemy') {
// 在敌人的窗口中不进行攻击检测
this.update_attack();
}
this.render();
}
...
}
Каждому игроку также нужна функция receive_attack
для представления полученной информации об атаке:
class Player extends AcGameObject {
...
destroy_fireball(uuid) {
// 删除火球
for (let i = 0; i < this.fire_balls.length; i++) {
let fire_ball = this.fire_balls[i];
if (fire_ball.uuid === uuid) {
fire_ball.destroy();
break;
}
}
}
...
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 < this.eps) {
// 半径小于eps认为已死
this.destroy();
return false;
}
this.damage_vx = Math.cos(theta);
this.damage_vy = Math.sin(theta);
this.damage_speed = damage * 90;
}
receive_attack(x, y, theta, damage, fireball_uuid, attacker) {
// 接收被攻击到的消息
attacker.destroy_fireball(fireball_uuid);
this.x = x;
this.y = y;
this.is_attacked(theta, damage);
}
...
}
Мы предполагаем, что игрок attacker
, который запускает огненный шар, - , а игрок, в которого попали, - . attackee
Положение пораженного также определяется окном атакующего, и огненный шар также должен исчезнуть в окнах других игроков после попадания в других игроков, поэтому Также необходимо метать огненные шары uuid
. Мы MultiPlayerSocket
реализуем send_attack
функцию и receive_attack
в классе:
class MultiPlayerSocket {
...
receive() {
let outer = this;
this.ws.onmessage = function(e) {
let data = JSON.parse(e.data); // 将字符串变回JSON
let uuid = data.uuid;
if (uuid === outer.uuid) return false; // 如果是给自己发送消息就直接过滤掉
let event = data.event;
if (event === 'create_player') {
// create_player路由
outer.receive_create_player(uuid, data.username, data.avatar);
} else if (event === 'move_to') {
// move_to路由
outer.receive_move_to(uuid, data.tx, data.ty);
} else if (event === 'shoot_fireball') {
// shoot_fireball路由
outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.fireball_uuid);
} else if (event === 'attack') {
// attack路由
outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.theta, data.damage, data.fireball_uuid);
}
};
}
...
send_attack(attackee_uuid, x, y, theta, damage, fireball_uuid) {
let outer = this;
this.ws.send(JSON.stringify({
'event': 'attack',
'uuid': outer.uuid,
'attackee_uuid': attackee_uuid,
'x': x,
'y': y,
'theta': theta,
'damage': damage,
'fireball_uuid': fireball_uuid,
}));
}
receive_attack(uuid, attackee_uuid, x, y, theta, damage, fireball_uuid) {
let attacker = this.get_player(uuid);
let attackee = this.get_player(attackee_uuid);
if (attacker && attackee) {
// 如果攻击者和被攻击者都还存在就判定攻击
attackee.receive_attack(x, y, theta, damage, fireball_uuid, attacker);
}
}
}
Затем реализуйте бэкэнд-функцию следующим образом:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from django.core.cache import cache
class MultiPlayer(AsyncWebsocketConsumer):
...
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'],
'theta': data['theta'],
'damage': data['damage'],
'fireball_uuid': data['fireball_uuid'],
}
)
async def receive(self, text_data):
data = json.loads(text_data)
print(data)
event = data['event']
if event == 'create_player': # 做一个路由
await self.create_player(data)
elif event == 'move_to': # move_to的路由
await self.move_to(data)
elif event == 'shoot_fireball': # shoot_fireball的路由
await self.shoot_fireball(data)
elif event == 'attack': # attack的路由
await self.attack(data)
FireBall
Наконец, вам нужно вызвать функцию синхронизации определения атаки в классе фаербола :
class FireBall extends AcGameObject {
// 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
...
}
start() {
}
update_move() {
...
}
update_attack() {
// 攻击碰撞检测
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攻击player
}
}
}
update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}
this.update_move();
if (this.player.character !== 'enemy') {
// 在敌人的窗口中不进行攻击检测
this.update_attack();
}
this.render();
}
get_dist(x1, y1, x2, 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);
if (this.playground.mode === 'multi mode') {
this.playground.mps.send_attack(player.uuid, player.x, player.y, theta, this.damage, this.uuid);
}
this.destroy();
}
render() {
...
}
on_destroy() {
...
}
}
4. Оптимизация и улучшение (доска подсказок игрока, компакт-диск навыков)
Ограничиваем игроков в перемещении, когда количество людей в комнате не достигло 3. Нам нужно AcGamePlayground
добавить в класс конечный автомат state
. Всего есть три состояния: waiting
, fighting
, over
, и статус каждого окна независим. Подсказка плата будет реализована позже. :
class AcGamePlayground {
...
// 显示playground界面
show(mode) {
...
this.mode = mode; // 需要将模式记录下来,之后玩家在不同的模式中需要调用不同的函数
this.state = 'waiting'; // waiting -> fighting -> over
this.notice_board = new NoticeBoard(this); // 提示板
this.player_count = 0; // 玩家人数
this.resize(); // 界面打开后需要resize一次,需要将game_map也resize
...
}
...
}
Затем мы реализуем доску подсказок, чтобы показать, сколько игроков ждут в текущей комнате. Создайте новый каталог ~/djangoapp/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的信息
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);
}
}
Каждый раз, когда создается игрок, player_count
число будет увеличиваться на 1. Когда количество игроков больше или равно 3, состояние игры будет преобразовано в Fighting
. Настройка Fighting
будет иметь эффект только при щелчке мыши или нажатии кнопка в состоянии, иначе она будет недействительна. Player
Внесите изменения в класс :
class Player extends AcGameObject {
...
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');
}
...
}
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 (outer.playground.state !== 'fighting')
return false; // 点击事件不往后传
...
}
});
$(window).keydown(function(e) {
if (outer.playground.state !== 'fighting')
return true;
...
});
}
...
}
Сейчас возможность атаковать в начале игры явно не подходит, поэтому необходимо еще и настроить так, чтобы он не мог атаковать в первые секунды игры, то есть умение остывает. Каждое окно имеет только свое время восстановления навыка, то есть видно только свое время восстановления. Теперь мы устанавливаем время восстановления навыка «Огненный шар» в одну секунду и Player
модифицируем его в классе:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
if (this.character === 'me') {
// 如果是自己的话则加上技能CD
this.fireball_coldtime = 1; // 单位: s
}
}
...
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 (outer.playground.state !== 'fighting')
return false; // 点击事件不往后传
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {
// 1表示左键,2表示滚轮,3表示右键
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty); // e.clientX/Y为鼠标点击坐标
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_move_to(tx, ty);
}
} 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 fire_ball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
}
outer.fireball_coldtime = 1; // 用完技能后重置冷却时间
}
outer.cur_skill = null; // 释放完一次技能后还原
}
});
$(window).keydown(function(e) {
if (outer.playground.state !== 'fighting')
return true;
if (e.which === 81 && outer.fireball_coldtime < outer.eps) {
// Q键
outer.cur_skill = 'fireball';
return false;
}
});
}
...
update_coldtime() {
// 更新技能冷却时间
this.fireball_coldtime -= this.timedelta / 1000;
this.fireball_coldtime = Math.max(this.fireball_coldtime, 0); // 防止变为负数
}
update() {
this.spent_time += this.timedelta / 1000; // 将这行代码从update_move函数移动到update函数中
this.update_move();
if (this.character === 'me' && this.playground.state === 'fighting') {
// 只有自己且开始战斗后才更新冷却时间
this.update_coldtime();
}
this.render();
}
...
}
Мы до сих пор не знаем, когда навык остынет, поэтому нам нужно добавить значок навыка и подсказку о компакт-диске. Мы можем имитировать другие игры MOBA и просто добавить слой покрытия CD на значок навыка. Предполагая, что наши ресурсы значков навыков хранятся в ~/djangoapp/game/static/image/playground
каталоге, мы Player
отображаем значок навыка в классе:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
if (this.character === 'me') {
// 如果是自己的话则加上技能CD
this.fireball_coldtime = 1; // 单位: s
this.fireball_img = new Image();
this.fireball_img.src = 'https://app4007.acapp.acwing.com.cn/static/image/playground/fireball.png'; // 技能图标资源链接
}
}
...
render() {
let scale = this.playground.scale; // 要将相对值恢复成绝对值
if (this.character !== 'robot') {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.img, (this.x - this.radius) * scale, (this.y - this.radius) * scale, this.radius * 2 * scale, this.radius * 2 * scale);
this.ctx.restore();
} else {
// AI
this.ctx.beginPath();
// 角度从0画到2PI,是否逆时针为false
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
if (this.character === 'me' && this.playground.state === 'fighting') {
this.render_skill_coldtime();
}
}
render_skill_coldtime() {
// 渲染技能图标与冷却时间
let x = 1.5, y = 0.95, r = 0.03;
let scale = this.playground.scale;
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x * scale, y * scale, r * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.fireball_img, (x - r) * scale, (y - r) * scale, r * 2 * scale, r * 2 * scale);
this.ctx.restore();
if (this.fireball_coldtime > 0) {
// 技能还在冷却中则绘制冷却蒙版
this.ctx.beginPath();
// 角度由冷却时间决定
let fireball_coldtime_ratio = this.fireball_coldtime / 1; // 剩余冷却时间占总冷却时间的比例
this.ctx.moveTo(x * scale, y * scale); // 设置圆心从(x, y)开始画
// 减去PI/2的目的是为了从PI/2处开始转圈,而不是从0度开始
// 最后的参数为false为取逆时针方向,反之为顺时针,但为true后相当于绘制的是冷却时间对立的另一段,因此需要调换一下冷却时间
this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - fireball_coldtime_ratio) - Math.PI / 2, true);
this.ctx.lineTo(x * scale, y * scale); // 画完之后向圆心画一条线
this.ctx.fillStyle = 'rgba(0, 0, 255, 0.6)';
this.ctx.fill();
}
}
on_destroy() {
if (this.character === 'me')
this.playground.state = 'over'; // 玩家寄了之后更新状态为over
for (let i = 0; i < this.playground.players.length; i++) {
if (this.playground.players[i] === this) {
this.playground.players.splice(i, 1);
break;
}
}
}
}
5. Флеш-навыки
Реализация навыка вспышки очень проста.Достаточно обратиться к предыдущему навыку огненного шара в целом.Сначала мы реализуем навык вспышки в автономном режиме и Player
реализуем его в классе:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
if (this.character === 'me') {
// 如果是自己的话则加上技能CD
this.fireball_coldtime = 1; // 单位: s
this.fireball_img = new Image();
this.fireball_img.src = 'https://app4007.acapp.acwing.com.cn/static/image/playground/fireball.png'; // 技能图标资源链接
this.blink_coldtime = 10; // 闪现技能冷却时间
this.blink_img = new Image();
this.blink_img.src = 'https://app4007.acapp.acwing.com.cn/static/image/playground/blink.png';
}
}
...
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 (outer.playground.state !== 'fighting')
return false; // 点击事件不往后传
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {
// 1表示左键,2表示滚轮,3表示右键
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty); // e.clientX/Y为鼠标点击坐标
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_move_to(tx, ty);
}
} 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 fire_ball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
}
outer.fireball_coldtime = 1; // 用完技能后重置冷却时间
} else if (outer.cur_skill === 'blink') {
outer.blink(tx, ty);
outer.blink_coldtime = 10;
}
outer.cur_skill = null; // 释放完一次技能后还原
}
});
$(window).keydown(function(e) {
if (outer.playground.state !== 'fighting')
return true;
if (e.which === 81 && outer.fireball_coldtime < outer.eps) {
// Q键
outer.cur_skill = 'fireball';
return false;
} else if (e.which === 70 && outer.blink_coldtime < outer.eps) {
// F键
outer.cur_skill = 'blink';
return false;
}
});
}
// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
...
blink(tx, ty) {
// 闪现到(tx, ty)
let x = this.x, y = this.y;
let dist = this.get_dist(x, y, tx, ty);
dist = Math.min(dist, 0.3); // 最大闪现距离为0.3
let theta = Math.atan2(ty - y, tx - x);
this.x += dist * Math.cos(theta);
this.y += dist * Math.sin(theta);
this.move_length = 0; // 闪现完之后应该停下来而不是继续移动
}
...
update_coldtime() {
// 更新技能冷却时间
this.fireball_coldtime -= this.timedelta / 1000;
this.fireball_coldtime = Math.max(this.fireball_coldtime, 0); // 防止变为负数
this.blink_coldtime -= this.timedelta / 1000;
this.blink_coldtime = Math.max(this.blink_coldtime, 0);
}
update() {
this.spent_time += this.timedelta / 1000; // 将这行代码从update_move函数移动到update函数中
this.update_move();
if (this.character === 'me' && this.playground.state === 'fighting') {
// 只有自己且开始战斗后才更新冷却时间
this.update_coldtime();
}
this.render();
}
render() {
let scale = this.playground.scale; // 要将相对值恢复成绝对值
if (this.character !== 'robot') {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.img, (this.x - this.radius) * scale, (this.y - this.radius) * scale, this.radius * 2 * scale, this.radius * 2 * scale);
this.ctx.restore();
} else {
// AI
this.ctx.beginPath();
// 角度从0画到2PI,是否逆时针为false
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
if (this.character === 'me' && this.playground.state === 'fighting') {
this.render_fireball_coldtime();
this.render_blink_coldtime();
}
}
render_fireball_coldtime() {
// 渲染火球技能图标与冷却时间
let x = 1.5, y = 0.95, r = 0.03;
let scale = this.playground.scale;
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x * scale, y * scale, r * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.fireball_img, (x - r) * scale, (y - r) * scale, r * 2 * scale, r * 2 * scale);
this.ctx.restore();
if (this.fireball_coldtime > 0) {
// 技能还在冷却中则绘制冷却蒙版
this.ctx.beginPath();
// 角度由冷却时间决定
let coldtime_ratio = this.fireball_coldtime / 1; // 剩余冷却时间占总冷却时间的比例
this.ctx.moveTo(x * scale, y * scale); // 设置圆心从(x, y)开始画
// 减去PI/2的目的是为了从PI/2处开始转圈,而不是从0度开始
// 最后的参数为false为取逆时针方向,反之为顺时针,但为true后相当于绘制的是冷却时间对立的另一段,因此需要调换一下冷却时间
this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - coldtime_ratio) - Math.PI / 2, true);
this.ctx.lineTo(x * scale, y * scale); // 画完之后向圆心画一条线
this.ctx.fillStyle = 'rgba(0, 0, 255, 0.6)';
this.ctx.fill();
}
}
render_blink_coldtime() {
// 渲染闪现技能图标与冷却时间
let x = 1.6, y = 0.95, r = 0.03;
let scale = this.playground.scale;
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x * scale, y * scale, r * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.blink_img, (x - r) * scale, (y - r) * scale, r * 2 * scale, r * 2 * scale);
this.ctx.restore();
if (this.blink_coldtime > 0) {
this.ctx.beginPath();
let coldtime_ratio = this.blink_coldtime / 10;
this.ctx.moveTo(x * scale, y * scale); // 设置圆心从(x, y)开始画
this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - coldtime_ratio) - Math.PI / 2, true);
this.ctx.lineTo(x * scale, y * scale); // 画完之后向圆心画一条线
this.ctx.fillStyle = 'rgba(0, 0, 255, 0.6)';
this.ctx.fill();
}
}
on_destroy() {
if (this.character === 'me')
this.playground.state = 'over'; // 玩家寄了之后更新状态为over
for (let i = 0; i < this.playground.players.length; i++) {
if (this.playground.players[i] === this) {
this.playground.players.splice(i, 1);
break;
}
}
}
}
Затем нам также необходимо синхронизировать флеш-навыки в многопользовательском режиме.Принцип тот же, что и синхронизация движения.Сначала MultiPlayerSocket
реализуем в классе фронтенд-функцию:
class MultiPlayerSocket {
...
receive() {
let outer = this;
this.ws.onmessage = function(e) {
let data = JSON.parse(e.data); // 将字符串变回JSON
let uuid = data.uuid;
if (uuid === outer.uuid) return false; // 如果是给自己发送消息就直接过滤掉
let event = data.event;
if (event === 'create_player') {
// create_player路由
outer.receive_create_player(uuid, data.username, data.avatar);
} else if (event === 'move_to') {
// move_to路由
outer.receive_move_to(uuid, data.tx, data.ty);
} else if (event === 'shoot_fireball') {
// shoot_fireball路由
outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.fireball_uuid);
} else if (event === 'attack') {
// attack路由
outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.theta, data.damage, data.fireball_uuid);
} else if (event === 'blink') {
// blink路由
outer.receive_blink(uuid, data.tx, data.ty);
}
};
}
...
send_blink(tx, ty) {
let outer = this;
this.ws.send(JSON.stringify({
'event': 'blink',
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
}));
}
receive_blink(uuid, tx, ty) {
let player = this.get_player(uuid);
if (player) {
player.blink(tx, ty);
}
}
}
Затем реализуйте бэкэнд и ~/djangoapp/game/consumers/multiplayer/index.py
внедрите его в файл:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from django.core.cache import cache
class MultiPlayer(AsyncWebsocketConsumer):
...
async def blink(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': 'group_send_event',
'event': 'blink',
'uuid': data['uuid'],
'tx': data['tx'],
'ty': data['ty'],
}
)
async def receive(self, text_data):
data = json.loads(text_data)
print(data)
event = data['event']
if event == 'create_player': # 做一个路由
await self.create_player(data)
elif event == 'move_to': # move_to的路由
await self.move_to(data)
elif event == 'shoot_fireball': # shoot_fireball的路由
await self.shoot_fireball(data)
elif event == 'attack': # attack的路由
await self.attack(data)
elif event == 'blink': # blink的路由
await self.blink(data)
Наконец, Player
просто вызовите функцию навыка трансляции flash в классе:
class Player extends AcGameObject {
...
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 (outer.playground.state !== 'fighting')
return false; // 点击事件不往后传
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {
// 1表示左键,2表示滚轮,3表示右键
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty); // e.clientX/Y为鼠标点击坐标
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_move_to(tx, ty);
}
} 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 fire_ball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
}
outer.fireball_coldtime = 1; // 用完技能后重置冷却时间
} else if (outer.cur_skill === 'blink') {
outer.blink(tx, ty);
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_blink(tx, ty);
}
outer.blink_coldtime = 10;
}
outer.cur_skill = null; // 释放完一次技能后还原
}
});
$(window).keydown(function(e) {
if (outer.playground.state !== 'fighting')
return true;
if (e.which === 81 && outer.fireball_coldtime < outer.eps) {
// Q键
outer.cur_skill = 'fireball';
return false;
} else if (e.which === 70 && outer.blink_coldtime < outer.eps) {
// F键
outer.cur_skill = 'blink';
return false;
}
});
}
...
}