球球作战
一个简简单单的射击游戏
W
向上移动
S
向下移动
A
向左移动
D
向右移动
鼠标左键
发射子弹
开始游戏
连接服务器中...
然后在index.js
中导入css
。
// src/client/index.js
import ‘./css/bootstrap-reboot.css’
import ‘./css/main.css’
在src/client/css
中创建对应的文件,其中bootstrap-reboot
是bootstrap
的重置基础样式的文件,这个可以在网络上下载,因为太长,本文就不贴出来了。
在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前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
最后
整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。
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)]