Node 开发一个多人对战的射击游戏,web快速开发平台

球球作战

一个简简单单的射击游戏


W 向上移动

S 向下移动

A 向左移动

D 向右移动

鼠标左键 发射子弹

开始游戏

连接服务器中...

然后在index.js中导入css

// src/client/index.js

import ‘./css/bootstrap-reboot.css’

import ‘./css/main.css’

src/client/css中创建对应的文件,其中bootstrap-rebootbootstrap的重置基础样式的文件,这个可以在网络上下载,因为太长,本文就不贴出来了。

main.css中编写对应的样式。

// src/client/css/main.css

html, body {

margin: 0;

padding: 0;

overflow: hidden;

width: 100%;

height: 100vh;

background: linear-gradient(to right bottom, rgb(154, 207, 223), rgb(100, 216, 89));

}

.hidden{

display: none !important;

}

#cnv{

width: 100%;

height: 100%;

}

.text-secondary{

color: #666;

}

code{

color: white;

background: rgb(236, 72, 72);

padding: 2px 10px;

border-radius: 5px;

}

hr {

border: 0;

border-top: 1px solid rgba(0, 0, 0, 0.1);

margin: 1rem 0;

width: 100%;

}

button {

font-size: 18px;

outline: none;

border: none;

color: black;

background-color: transparent;

padding: 5px 20px;

border-radius: 3px;

transition: background-color 0.2s ease;

}

button:hover {

background-color: rgb(141, 218, 134);

color: white;

}

button:focus {

outline: none;

}

#home p{

margin-bottom: 5px;

}

#home{

position: fixed;

top: 50%;

left: 50%;

transform: translateY(-50%) translateX(-50%);

padding: 20px 30px;

background-color: white;

display: flex;

flex-direction: column;

align-items: center;

border-radius: 5px;

text-align: center;

}

#home input {

font-size: 18px;

outline: none;

border: none;

border-bottom: 1px solid #dedede;

margin-bottom: 5px;

padding: 3px;

text-align: center;

}

#home input:focus{

border-bottom: 1px solid #8d8d8d;

}

#home .content{

display: flex;

justify-content: space-between;

align-items: center;

}

#home .content .play{

width: 200px;

margin-left: 50px;

}

#home .content .connect{

margin-left: 50px;

}

最后我们就可以得到下面这张图的效果了。

image.png

编写游戏开始的逻辑


我们先创建一个util.js来存放一些工具函数。

// src/client/util.js

export function $(elem){

return document.querySelector(elem)

}

然后在index.js中编写对应的逻辑代码。

// src/client/index.js

import { connect, play } from ‘./networking’

import { $ } from ‘./util’

Promise.all([

connect()

]).then(() => {

// 隐藏连接服务器显示输入框及按键

$(‘.connect’).classList.add(‘hidden’)

$(‘.play’).classList.remove(‘hidden’)

// 并且默认聚焦输入框

$(‘#home input’).focus();

// 游戏开始按钮监听点击事件

$(‘#play-button’).onclick = () => {

// 判断输入框的值是否为空

let val = $(‘#home input’).value;

if(val.replace(/\s*/g, ‘’) === ‘’) {

alert(‘名称不能为空’)

return;

}

// 游戏开始,隐藏开始界面

$(‘#home’).classList.add(‘hidden’)

play(val)

}

}).catch(console.error)

上面的代码已经可以正常的开始游戏了,但是游戏开始了,没有画面。

所以,我们现在来开发一下渲染画面的代码。

加载资源


我们都知道canvas绘制图片需要图片加载完毕,不然的话会啥也没有,所以我们先编写一个加载所有图片的代码。

图片文件存储在public/assets

// src/client/asset.js

// 需要加载的资源

const ASSET_NAMES = [

‘ball.svg’,

‘aim.svg’

]

// 将下载好的图片文件保存起来供canvas使用

const assets = {};

// 每一张图片都是通过promise进行加载的,所有图片加载成功后,Promise.all就会结束

const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset))

function downloadAsset(assetName){

return new Promise(resolve => {

const asset = new Image();

asset.onload = () => {

console.log(Downloaded ${assetName})

assets[assetName] = asset;

resolve();

}

asset.src = /assets/${assetName}

})

}

export const downloadAssets = () => downloadPromise;

export const getAsset = assetName => assets[assetName]

接下来在index.js中引入asset.js

// src/client/index.js

import { downloadAssets } from ‘./asset’

Promise.all([

connect(),

downloadAssets()

]).then(() => {

// …

}).catch(console.error)

这个时候,我们在页面中就可以看到这样的输出了。

image.png

图片可以去iconfont或是在线体验的network或是github中下载。

绘制游戏对象


我们新建一个render.js文件,在其中编写对应的绘制代码。

// src/client/render.js

import { MAP_SIZE, PLAYER } from ‘…/shared/constants’

import { getAsset } from ‘./asset’

import { getCurrentState } from ‘./state’

import { $ } from ‘./util’

const cnv = $(‘#cnv’)

const ctx = cnv.getContext(‘2d’)

function setCanvasSize(){

cnv.width = window.innerWidth;

cnv.height = window.innerHeight;

}

// 这里将默认设置一次canvas宽高,当屏幕缩放的时候也会设置一次

setCanvasSize();

window.addEventListener(‘resize’, setCanvasSize)

// 绘制函数

function render(){

const { me, others, bullets } = getCurrentState();

if(!me){

return;

}

}

// 这里将启动渲染函数的定时器,将其导出,我们在index.js中使用

let renderInterval = null;

export function startRendering(){

renderInterval = setInterval(render, 1000 / 60);

}

export function stopRendering(){

ctx.clearRect(0, 0, cnv.width, cnv.height)

clearInterval(renderInterval);

}

可以看到上面我们引入state.js中的getCurrentState函数,这个函数将获取最新服务器返回的数据对象。

// src/client/state.js

const gameUpdates = [];

export function processGameUpdate(update){

gameUpdates.push(update)

}

export function getCurrentState(){

return gameUpdates[gameUpdates.length - 1]

}

绘制背景

因为游戏中的地图是一个大地图,一个屏幕是装不下的,所以玩家移动需要一个参照物,这里使用一个渐变的圆来做参照物。

// src/client/render.js

function render(){

// …

// 绘制背景圆

renderBackground(me.x, me.y);

// 绘制一个边界

ctx.strokeStyle = ‘black’

ctx.lineWidth = 1;

// 默认边界左上角在屏幕中心,减去人物的x/y算出相对于人物的偏移

ctx.strokeRect(cnv.width / 2 - me.x, cnv.height / 2 - me.y, MAP_SIZE, MAP_SIZE)

}

function renderBackground(x, y){

// 假设背景圆的位置在屏幕左上角,那么cnv.width/height / 2就会将这个圆定位在屏幕中心

// MAP_SIZE / 2 - x/y 地图中心与玩家的距离,这段距离就是背景圆圆心正确的位置

const backgroundX = MAP_SIZE / 2 - x + cnv.width / 2;

const backgroundY = MAP_SIZE / 2 - y + cnv.height / 2;

const bgGradient = ctx.createRadialGradient(

backgroundX,

backgroundY,

MAP_SIZE / 10,

backgroundX,

backgroundY,

MAP_SIZE / 2

)

bgGradient.addColorStop(0, ‘rgb(100, 216, 89)’)

bgGradient.addColorStop(1, ‘rgb(154, 207, 223)’)

ctx.fillStyle = bgGradient;

ctx.fillRect(0, 0, cnv.width, cnv.height)

}

上面的代码实现的效果就是下图。

我们玩家的位置在服务器中设置的是随机数字,所以每次进入游戏都是随机的位置。

image.png

绘制玩家

接下来就是绘制玩家了,依旧是在render.js中编写对应的代码。

// src/client/render.js

function render(){

// …

// 绘制所有的玩家

// 第一个参数是对照位置的数据,第二个参数是玩家渲染的数据

renderPlayer(me, me);

others.forEach(renderPlayer.bind(null, me));

}

function renderPlayer(me, player){

const { x, y } = player;

// 默认将玩家渲染在屏幕中心,然后将位置设置上去,再计算相对于自己的相对位置,就是正确在屏幕的位置了

const canvasX = cnv.width / 2 + x - me.x;

const canvasY = cnv.height / 2 + y - me.y;

ctx.save();

ctx.translate(canvasX, canvasY);

ctx.drawImage(

getAsset(‘ball.svg’),

-PLAYER.RADUIS,

-PLAYER.RADUIS,

PLAYER.RADUIS * 2,

PLAYER.RADUIS * 2

)

ctx.restore();

// 绘制血条背景

ctx.fillStyle = ‘white’

ctx.fillRect(

canvasX - PLAYER.RADUIS,

canvasY - PLAYER.RADUIS - 8,

PLAYER.RADUIS * 2,

4

)

// 绘制血条

ctx.fillStyle = ‘red’

ctx.fillRect(

canvasX - PLAYER.RADUIS,

canvasY - PLAYER.RADUIS - 8,

PLAYER.RADUIS * 2 * (player.hp / PLAYER.MAX_HP),

4

)

// 绘制玩家的名称

ctx.fillStyle = ‘white’

ctx.textAlign = ‘center’;

ctx.font = “20px ‘微软雅黑’”

ctx.fillText(player.username, canvasX, canvasY - PLAYER.RADUIS - 16)

}

这样就可以将玩家正确的绘制出来了。

image.png

image.png

上面两张图,是我打开两个页面进入游戏的两名玩家,可以看出它们分别以自己为中心,其他的玩家相对于它进行了绘制。

游戏玩法开发

======

添加移动交互


既然玩家我们绘制出来了,那么就可以让它开始移动起来了。

我们创建一个input.js来编写对应的输入交互代码。

// src/client/input.js

// 发送信息给后端

import { emitControl } from “./networking”;

function onKeydown(ev){

let code = ev.keyCode;

switch(code){

case 65:

emitControl({

action: ‘move-left’,

data: false

})

break;

case 68:

emitControl({

action: ‘move-right’,

data: true

})

break;

case 87:

emitControl({

action: ‘move-top’,

data: false

})

break;

case 83:

emitControl({

action: ‘move-bottom’,

data: true

})

break;

}

}

function onKeyup(ev){

let code = ev.keyCode;

switch(code){

case 65:

emitControl({

action: ‘move-left’,

data: 0

})

break;

case 68:

emitControl({

action: ‘move-right’,

data: 0

})

break;

case 87:

emitControl({

action: ‘move-top’,

data: 0

})

break;

case 83:

emitControl({

action: ‘move-bottom’,

data: 0

})

break;

}

}

export function startCapturingInput(){

window.addEventListener(‘keydown’, onKeydown);

window.addEventListener(‘keyup’, onKeyup);

}

export function stopCapturingInput(){

window.removeEventListener(‘keydown’, onKeydown);

window.removeEventListener(‘keyup’, onKeyup);

}

// src/client/networking.js

// …

// 发送信息给后端

export const emitControl = data => {

socket.emit(Constants.MSG_TYPES.INPUT, data);

}

上面的代码很简单,通过判断W/S/A/D四个按键发送信息给后端。

后端进行处理传递给玩家对象,然后在游戏更新中使玩家移动。

// src/servers/core/game.js

class Game{

// …

update(){

const now = Date.now();

const dt = (now - this.lastUpdateTime) / 1000;

this.lastUpdateTime = now;

// 每次游戏更新告诉玩家对象,你要更新了

Object.keys(this.players).map(playerID => {

const player = this.players[playerID]

player.update(dt)

})

}

handleInput(socket, item){

const player = this.players[socket.id];

if(player){

let data = item.action.split(‘-’);

let type = data[0];

let value = data[1];

switch(type){

case ‘move’:

// 这里是为了防止前端发送1000/-1000这种数字,会导致玩家移动飞快

player.move[value] = typeof item.data === ‘boolean’

? item.data ? 1 : -1

: 0

break;

}

}

}

}

然后在player.js中加入对应的移动代码。

// src/servers/objects/player.js

class Player extends Item{

constructor(data){

super(data)

this.move = {

left: 0, right: 0,

top: 0, bottom: 0

};

// …

}

update(dt){

// 这里的dt是每次游戏更新的时间,乘于dt将会60帧也就是一秒移动speed的值

this.x += (this.move.left + this.move.right) * this.speed * dt;

this.y += (this.move.top + this.move.bottom) * this.speed * dt;

}

// …

}

module.exports = Player;

通过上面的代码,我们就实现了玩家移动的逻辑了,下面我们看看效果。

5.gif

可以看出,我们可以飞出地图之外,我们在player.js中添加对应的限制代码。

// src/servers/objects/player.js

class Player extends Item{

// …

update(dt){

this.x += (this.move.left + this.move.right) * this.speed * dt;

this.y += (this.move.top + this.move.bottom) * this.speed * dt;

// 在地图最大尺寸和自身位置比较时,不能大于地图最大尺寸

// 在地图开始0位置和自身位置比较时,不能小于0

this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x))

this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y))

}

// …

}

module.exports = Player;

增加发送子弹


既然我们的人物已经可以移动了,那么玩家间对抗的工具“子弹”那肯定是不能少的,现在我们就来开发吧。

我们先在前端添加发送开枪意图的代码。

// src/client/input.js

// 这里使用atan2获取鼠标相对屏幕中心的角度

function getMouseDir(ev){

const dir = Math.atan2(ev.clientX - window.innerWidth / 2, ev.clientY - window.innerHeight / 2);

return dir;

}

// 每次鼠标移动,发送方向给后端保存

function onMousemove(ev){

if(ev.button === 0){

emitControl({

action: ‘dir’,

data: getMouseDir(ev)

})

}

}

// 开火

function onMousedown(ev){

if(ev.button === 0){

emitControl({

action: ‘bullet’,

data: true

})

}

}

// 停火

function onMouseup(ev){

if(ev.button === 0){

emitControl({

action: ‘bullet’,

data: false

})

}

}

export function startCapturingInput(){

window.addEventListener(‘mousedown’, onMousedown)

window.addEventListener(‘mousemove’, onMousemove)

window.addEventListener(‘mouseup’, onMouseup)

}

export function stopCapturingInput(){

window.removeEventListener(‘mousedown’, onMousedown)

window.addEventListener(‘mousemove’, onMousemove)

window.removeEventListener(‘mouseup’, onMouseup)

}

然后在后端中编写对应的代码。

// src/servers/core/game.js

class Game{

// …

update(){

// …

// 如果子弹飞出地图或是已经达到人物身上,就过滤掉

this.bullets = this.bullets.filter(item => !item.isOver)

// 为每一个子弹更新

this.bullets.map(bullet => {

bullet.update(dt);

})

Object.keys(this.players).map(playerID => {

const player = this.players[playerID]

// 在人物对象中添加发射子弹

const bullet = player.update(dt)

if(bullet){

this.bullets.push(bullet);

}

})

}

handleInput(socket, item){

const player = this.players[socket.id];

if(player){

let data = item.action.split(‘-’);

let type = data[0];

let value = data[1];

switch(type){

case ‘move’:

player.move[value] = typeof item.data === ‘boolean’

? item.data ? 1 : -1

: 0

break;

// 更新鼠标位置

case ‘dir’:

player.fireMouseDir = item.data;

break;

// 开火/停火

case ‘bullet’:

player.fire = item.data;

break;

}

}

}

}

module.exports = Game;

game.js中已经编写好了子弹的逻辑了,现在只需要在player.js中返回一个bullet对象就可以成功发射了。

// src/servers/objects/player.js

const Bullet = require(‘./bullet’);

class Player extends Item{

constructor(data){

super(data)

// …

// 开火

this.fire = false;

this.fireMouseDir = 0;

this.fireTime = 0;

}

update(dt){

// …

// 每帧都减少开火延迟

this.fireTime -= dt;

// 判断是否开火

if(this.fire != false){

// 如果没有延迟了就返回一个bullet对象

if(this.fireTime <= 0){

// 将延迟重新设置

this.fireTime = Constants.PLAYER.FIRE;

// 创建一个bullet对象,将自身的id传递过去,后面做碰撞的时候,就自己发射的子弹就不会打到自己

return new Bullet(this.id, this.x, this.y, this.fireMouseDir);

}

}

}

// …

}

module.exports = Player;

对应的bullet.js文件也要补全一下。

// src/servers/objects/bullet.js

const shortid = require(‘shortid’)

const Constants = require(‘…/…/shared/constants’);

const Item = require(‘./item’)

class Bullet extends Item{

constructor(parentID, x, y, dir){

super({

id: shortid(),

x, y,

w: Constants.BULLET.RADUIS,

h: Constants.BULLET.RADUIS,

});

this.rotate = 0;

this.dir = dir;

this.parentID = parentID;

this.isOver = false;

}

update(dt){

// 使用三角函数将鼠标位置计算出对应的x/y值

this.x += dt * Constants.BULLET.SPEED * Math.sin(this.dir);

this.y += dt * Constants.BULLET.SPEED * Math.cos(this.dir);

// 这里是为了让子弹有一个旋转功能,一秒转一圈

this.rotate += dt * 360;

// 离开地图就将isOver设置为true,在game.js中就会过滤

if(this.x < 0 || this.x > Constants.MAP_SIZE

|| this.y < 0 || this.y > Constants.MAP_SIZE){

this.isOver = true;

}

}

serializeForUpdate(){

return {

…(super.serializeForUpdate()),

rotate: this.rotate

}

}

}

module.exports = Bullet;

这里引入了一个shortid库,是创建一个随机数的作用

使用npm install shortid \--save安装

这个时候,我们就可以正常发射子弹,但是还不能看见子弹。

那是因为没有写对应的绘制代码。

// src/client/render.js

function render(){

// …

bullets.map(renderBullet.bind(null, me))

// …

}

function renderBullet(me, bullet){

const { x, y, rotate } = bullet;

ctx.save();

// 偏移到子弹相对人物的位置

ctx.translate(cnv.width / 2 + x - me.x, cnv.height / 2 + y - me.y)

// 旋转

ctx.rotate(Math.PI / 180 * rotate)

// 绘制子弹

ctx.drawImage(

getAsset(‘bullet.svg’),

-BULLET.RADUIS,

-BULLET.RADUIS,

BULLET.RADUIS * 2,

BULLET.RADUIS * 2

)

ctx.restore();

}

这个时候,我们就将发射子弹的功能完成了。

来看看效果吧。

6.gif

碰撞检测


既然完成了玩家的移动及发送子弹逻辑,现在就可以开发对战最重要的碰撞检测了。

我们直接在game.js中添加。

// src/servers/core/game.js

class Game{

// …

update(){

// …

// 将玩家及子弹传入进行碰撞检测

this.collisions(Object.values(this.players), this.bullets);

Object.keys(this.sockets).map(playerID => {

const socket = this.sockets[playerID]

const player = this.players[playerID]

// 如果玩家的血量低于等于0就告诉他游戏结束,并将其移除游戏

if(player.hp <= 0){

socket.emit(Constants.MSG_TYPES.GAME_OVER)

this.disconnect(socket);

}

})

// …

}

collisions(players, bullets){

for(let i = 0; i < bullets.length; i++){

for(let j = 0; j < players.length; j++){

let bullet = bullets[i];

let player = players[j];

// 自己发射的子弹不能达到自己身上

// distanceTo是一个使用勾股定理判断物体与自己的距离,如果距离小于玩家与子弹的半径就是碰撞了

if(bullet.parentID !== player.id

&& player.distanceTo(bullet) <= Constants.PLAYER.RADUIS + Constants.BULLET.RADUIS

){

// 子弹毁灭

bullet.isOver = true;

// 玩家扣血

player.takeBulletDamage();

// 这里判断给最后一击使其死亡的玩家加分

if(player.hp <= 0){

this.players[bullet.parentID].score++;

}

break;

}

}

}

}

// …

}

module.exports = Game;

接下来在前端中添加游戏结束的逻辑。

// src/client/index.js

// …

import { startRendering, stopRendering } from ‘./render’

import { startCapturingInput, stopCapturingInput } from ‘./input’

Promise.all([

connect(gameOver),

downloadAssets()

]).then(() => {

// …

}).catch(console.error)

function gameOver(){

// 停止渲染

stopRendering();

// 停止监听

stopCapturingInput();

// 将开始界面显示出来

$(‘#home’).classList.remove(‘hidden’);

alert(‘你GG了,重新进入游戏吧。’);

}

这个时候我们就可以正常的进行游戏了。

来看看效果。

8.gif

排行榜功能


既然我们已经完成了正常的游戏基本操作,那么现在需要一个排行来让玩家有游戏体验(啊哈哈哈)。

我们先在前端把排行榜显示出来。

我们先在后端添加返回排行榜的数据。

// src/servers/core/game.js

class Game{

// …

createUpdate(player){

// …

return {

// …

leaderboard: this.getLeaderboard()

}

}

getLeaderboard(){

return Object.values(this.players)

.sort((a, b) => b.score - a.score)

.slice(0, 10)

.map(item => ({ username: item.username, score: item.score }))

}

}

module.exports = Game;

然后在前端中编写一下排行榜的样式。

// src/client/html/index.html

// …

排名 姓名 积分

// …

// src/client/css/main.css

// …

.ranking{

position: fixed;

width: 300px;

background: #333;

top: 0;

left: 0;

color: white;

padding: 10px;

}

.ranking table{

border: 0;

border-collapse: 0;

width: 100%;

}

再编写一个渲染数据的函数在render.js中。

// src/client/render.js

// …

export function updateRanking(data){

let str = ‘’;

data.map((item, i) => {

str += `

${i + 1} ${item.username} ${item.score}

`

})

$(‘.ranking table tbody’).innerHTML = str;

}

最后在state.js中使用这个函数。

// src/client/state.js

import { updateRanking } from “./render”;

const gameUpdates = [];

export function processGameUpdate(update){

gameUpdates.push(update)

updateRanking(update.leaderboard)

}

// …

现在渲染排行榜是没有问题了,现在到index.js中管理一下排行榜的显示隐藏。

// src/client/index.js

// …

Promise.all([

connect(gameOver),

downloadAssets()

]).then(() => {

// …

$(‘#play-button’).onclick = () => {

// …

$(‘.ranking’).classList.remove(‘hidden’)

// …

}

}).catch(console.error)

function gameOver(){

// …

$(‘.ranking’).classList.add(‘hidden’)

// …

}

写到这里,排行榜的功能就完成了。

image.png

道具开发


当然游戏现在这样游戏性还是很差的,我们来加几个道具增加一点游戏性吧。

先将prop.js完善吧。

// src/servers/objects/prop.js

const Constants = require(‘…/…/shared/constants’)

const Item = require(‘./item’)

class Prop extends Item{

constructor(type){

// 随机位置

const x = (Math.random() * .5 + .25) * Constants.MAP_SIZE;

const y = (Math.random() * .5 + .25) * Constants.MAP_SIZE;

super({

x, y,

w: Constants.PROP.RADUIS,

h: Constants.PROP.RADUIS

});

this.isOver = false;

// 什么类型的buff

this.type = type;

// 持续10秒

this.time = 10;

}

// 这个道具对玩家的影响

add(player){

switch(this.type){

case ‘speed’:

player.speed += 500;

break;

}

}

// 移除这个道具时将对玩家的影响消除

remove(player){

switch(this.type){

case ‘speed’:

player.speed -= 500;

break;

}

}

// 每帧更新

update(dt){

this.time -= dt;

}

serializeForUpdate(){

return {

…(super.serializeForUpdate()),

type: this.type,

time: this.time

}

}

}

module.exports = Prop;

然后我们在game.js中添加定时添加道具的逻辑。

// src/servers/core/game.js

const Constants = require(“…/…/shared/constants”);

const Player = require(“…/objects/player”);

const Prop = require(“…/objects/prop”);

class Game{

constructor(){

// …

// 增加一个保存道具的数组

this.props = [];

// …

// 添加道具的计时

this.createPropTime = 0;

setInterval(this.update.bind(this), 1000 / 60);

}

update(){

// …

// 这个定时为0时添加

this.createPropTime -= dt;

// 过滤掉已经碰撞后的道具

this.props = this.props.filter(item => !item.isOver)

// 道具大于10个时不添加

if(this.createPropTime <= 0 && this.props.length < 10){

this.createPropTime = Constants.PROP.CREATE_TIME;

this.props.push(new Prop(‘speed’))

}

// …

this.collisionsBullet(Object.values(this.players), this.bullets);

this.collisionsProp(Object.values(this.players), this.props)

// …

}

// 玩家与道具的碰撞检测

collisionsProp(players, props){

for(let i = 0; i < props.length; i++){

for(let j = 0; j < players.length; j++){

let prop = props[i];

let player = players[j];

if(player.distanceTo(prop) <= Constants.PLAYER.RADUIS + Constants.PROP.RADUIS){

// 碰撞后,道具消失

prop.isOver = true;

// 玩家添加这个道具的效果

player.pushBuff(prop);

break;

}

}

}

}

// 这里是之前的collisions,为了和碰撞道具区分

collisionsBullet(players, bullets){

// …

}

createUpdate(player){

// …

return {

// …

props: this.props.map(prop => prop.serializeForUpdate())

}

}

}

module.exports = Game;

这里可以将碰撞检测进行优化,将其改造成任何场景都可以使用的碰撞函数,这里是为了方便就直接复制成两个。

接下来在player.js添加对应的函数。

// src/servers/objects/player.js

const Item = require(‘./item’)

const Constants = require(‘…/…/shared/constants’);

const Bullet = require(‘./bullet’);

class Player extends Item{

// …

update(dt){

// …

// 判断buff是否失效

this.buffs = this.buffs.filter(item => {

if(item.time > 0){

return item;

} else {

item.remove(this);

}

})

// buff的持续时间每帧都减少

this.buffs.map(buff => buff.update(dt));

// …

}

// 添加

pushBuff(prop){

this.buffs.push(prop);

prop.add(this);

}

// …

serializeForUpdate(){

return {

// …

buffs: this.buffs.map(item => item.serializeForUpdate())

}

}

}

module.exports = Player;

后端需要做的功能已经完成了,现在到前端中添加绘制方面的代码。

// src/client/render.js

// …

function render(){

const { me, others, bullets, props } = getCurrentState();

if(!me){

return;

}

// …

// 绘制道具

props.map(renderProp.bind(null, me))

// …

}

// …

// 绘制道具

function renderProp(me, prop){

const { x, y, type } = prop;

ctx.save();

ctx.drawImage(

getAsset(${type}.svg),

cnv.width / 2 + x - me.x,

cnv.height / 2 + y - me.y,

PROP.RADUIS * 2,

PROP.RADUIS * 2

)

ctx.restore();

}

function renderPlayer(me, player){

// …

// 显示玩家已经领取到的道具

player.buffs.map((buff, i) => {

ctx.drawImage(

getAsset(${buff.type}.svg),

canvasX - PLAYER.RADUIS + i * 22,

canvasY + PLAYER.RADUIS + 16,

20, 20

)

})

}

这个时候,加速道具就完成啦。

如果你需要添加更多道具,可以在prop.js中进行添加,并且在game.js中生成道具的时候把speed改为随机道具的type

完成后的效果。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

最后

整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》

前端面试题宝典

前端校招面试题详解

Update())

}

}

}

module.exports = Game;

这里可以将碰撞检测进行优化,将其改造成任何场景都可以使用的碰撞函数,这里是为了方便就直接复制成两个。

接下来在player.js添加对应的函数。

// src/servers/objects/player.js

const Item = require(‘./item’)

const Constants = require(‘…/…/shared/constants’);

const Bullet = require(‘./bullet’);

class Player extends Item{

// …

update(dt){

// …

// 判断buff是否失效

this.buffs = this.buffs.filter(item => {

if(item.time > 0){

return item;

} else {

item.remove(this);

}

})

// buff的持续时间每帧都减少

this.buffs.map(buff => buff.update(dt));

// …

}

// 添加

pushBuff(prop){

this.buffs.push(prop);

prop.add(this);

}

// …

serializeForUpdate(){

return {

// …

buffs: this.buffs.map(item => item.serializeForUpdate())

}

}

}

module.exports = Player;

后端需要做的功能已经完成了,现在到前端中添加绘制方面的代码。

// src/client/render.js

// …

function render(){

const { me, others, bullets, props } = getCurrentState();

if(!me){

return;

}

// …

// 绘制道具

props.map(renderProp.bind(null, me))

// …

}

// …

// 绘制道具

function renderProp(me, prop){

const { x, y, type } = prop;

ctx.save();

ctx.drawImage(

getAsset(${type}.svg),

cnv.width / 2 + x - me.x,

cnv.height / 2 + y - me.y,

PROP.RADUIS * 2,

PROP.RADUIS * 2

)

ctx.restore();

}

function renderPlayer(me, player){

// …

// 显示玩家已经领取到的道具

player.buffs.map((buff, i) => {

ctx.drawImage(

getAsset(${buff.type}.svg),

canvasX - PLAYER.RADUIS + i * 22,

canvasY + PLAYER.RADUIS + 16,

20, 20

)

})

}

这个时候,加速道具就完成啦。

如果你需要添加更多道具,可以在prop.js中进行添加,并且在game.js中生成道具的时候把speed改为随机道具的type

完成后的效果。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-CkO2ASnX-1711949152130)]
[外链图片转存中…(img-t8vQCXHM-1711949152130)]
[外链图片转存中…(img-ipJpJpfH-1711949152131)]
[外链图片转存中…(img-KwtOoJUM-1711949152131)]
[外链图片转存中…(img-89KNiKZ9-1711949152131)]
[外链图片转存中…(img-xwVs6Sey-1711949152132)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-V286TSmz-1711949152132)]

最后

整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》

[外链图片转存中…(img-PPW5Cbq2-1711949152132)]

[外链图片转存中…(img-P8NFDMs3-1711949152133)]

[外链图片转存中…(img-rBVlnYZi-1711949152133)]

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
开发一个魔兽对战平台,你需要考虑以下几个步骤: 1. 确定需求:首先,明确你的对战平台的功能和特性。确定你想要支持的游戏模式、角色选择、战斗规则等。 2. 技术选择:选择合适的技术栈来开发你的对战平台。例如,你可以使用前端技术如 HTML、CSS 和 JavaScript 来构建用户界面,后端技术如 Node.js 或者其他服务器端语言来处理游戏逻辑和数据库操作。 3. 设计数据库:设计一个合适的数据库结构来存储用户信息、游戏数据等。你可以选择关系型数据库如 MySQL 或非关系型数据库如 MongoDB 来存储数据。 4. 开发用户界面:使用前端技术构建用户界面,包括登录/注册页面、角色选择页面、对战大厅、战斗界面等。你可以使用框架如 Vue.js 或 React 来简化开发过程。 5. 实现游戏逻辑:根据你的需求,开发游戏逻辑部分,包括角色属性、技能系统、计分规则等。确保游戏逻辑的稳定性和公平性。 6. 实现多人对战:为了实现多人对战,你可以使用 WebSocket 或者其他实时通信技术来处理玩家之间的实时交互和数据同步。 7. 测试和优化:进行系统测试和性能优化,确保你的对战平台稳定运行并具有良好的用户体验。 8. 部署和发布:将你的对战平台部署到服务器上,并确保它可以通过互联网访问。你可以选择使用云服务提供商如 AWS 或者阿里云来进行部署。 以上仅是开发魔兽对战平台的一般步骤,具体的实现可能会因项目需求和技术选择而有所不同。希望这些步骤可以为你提供一个大致的指导。祝你开发成功!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值