Node 开发一个多人对战的射击游戏 - 1

Node 开发一个多人对战的射击游戏 - 1

首页休闲益智射击球球更新时间:2024-07-18

相信大家都是知道游戏的吧。

这玩意还是很有意思的,无论是超级玛丽,还是魂斗罗,亦或者是王者荣耀以及阴阳师。

当然,这篇文章不涉及到那么牛逼的游戏,这里就简单的做一个小游戏吧。

先给它取个名字,就叫“球球作战”吧。

咳咳,简单易懂嘛

玩法

任何人进入游戏输入名字然后就可以连接进入游戏,控制一个小球。

你可以操作这个小球进行一些动作,比如:移动,发射子弹。

通过*死其他玩家来获取积分,并在排行榜上进行排名。

其实这类游戏有一个统一的名称,叫做IO类游戏,在这个网站中有大量的这类游戏:https://iogames.space/

这个游戏的github地址:https://github.com/lionet1224/node-game

在线体验: http://120.77.44.111:3000/

演示GIF:

准备工作

首先制作这个游戏,我们需要的技术为:

并且你需要对以下技术有一定了解:

其实本来想使用deno和ts来开发的,但是因为我对这两项技术都是半生不熟的阶段,所以就不拿出来献丑了。

游戏架构

后端服务需要做的是:

前端需要做的是:

这也是典型的状态同步方式开发游戏。

后端服务搭建开发

因为前端是通过后端的数据驱动的,所以我们就先开发后端。

搭建起一个Express服务

首先我们需要下载express,在根目录下输入以下命令:

//创建一个package.json文件 >npminit //安装并且将其置入package.json文件中的依赖中 >npminstallexpresssocket.io--save //安装并置入package.json的开发依赖中 >npminstallcross-envnodemon--save-dev

这里我们也可以使用cnpm进行安装

然后在根目录中疯狂建文件夹以及文件。

image.png

我们就可以得出以上的文件啦。

解释一下分别是什么东西:

编写基本代码

然后我们在server.js中编写启动服务的相关代码。

//server.js //引入各种模块 constexpress=require('express') constsocketio=require('socket.io'); constapp=express(); constSocket=require('./core/socket'); constGame=require('./core/game'); //启动服务 constport=process.env.PORT||3000; constserver=app.listen(3000,()=>{ console.log('ServerListeningonport:' port) }) //实例游戏类 constgame=newGame; //监听socket服务 constio=socketio(server) //将游戏以及io传入创建的socket类来统一管理 constsocket=newSocket(game,io); //监听连接进入游戏的回调 io.on('connect',item=>{ socket.listen(item) })

上面的代码还引入了两个其他文件core/game、core/socket。

这两个文件中的代码,我大致的编写了一下。

//core/game.js classGame{ constructor(){ //保存玩家的socket信息 this.sockets={} //保存玩家的游戏对象信息 this.players={}; //子弹 this.bullets=[]; //最后一次执行时间 this.lastUpdateTime=Date.now(); //是否发送给前端数据,这里将每两帧发送一次数据 this.shouldSendUpdate=false; //游戏更新 setInterval(this.update.bind(this),1000/60); } update(){ } //玩家加入游戏 joinGame(){ } //玩家断开游戏 disconnect(){ } } module.exports=Game;

//core/socket.js constConstants=require('../../shared/constants') classSocket{ constructor(game,io){ this.game=game; this.io=io; } listen(){ //玩家成功连接socket服务 console.log(`Playerconnected!SocketId:${socket.id}`) } } module.exports=Socket

在core/socket中引入了常量文件,我们来看看我在其中是怎么定义的。

//shared/constants.js module.exports=Object.freeze({ //玩家的数据 PLAYER:{ //最大生命 MAX_HP:100, //速度 SPEED:500, //大小 RADUIS:50, //开火频率,0.1秒一发 FIRE:.1 }, //子弹 BULLET:{ //子弹速度 SPEED:1500, //子弹大小 RADUIS:20 }, //道具 PROP:{ //生成时间 CREATE_TIME:10, //大小 RADUIS:30 }, //地图大小 MAP_SIZE:5000, //socket发送消息的函数名 MSG_TYPES:{ JOIN_GAME:1, UPDATE:2, INPUT:3 } })

Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。- MDN

通过上面的四个文件的代码,我们已经拥有了一个具备基本功能的后端服务结构了。

接下来就来将它启动起来吧。

创建启动命令

在package.json中编写启动命令。

//package.json { //... "scripts":{ "dev":"cross-envNODE_ENV=developmentnodemonsrc/servers/server.js", "start":"cross-envNODE_ENV=productionnodemonsrc/servers/server.js" } //.. }

这里的两个命令dev和start都使用到了cross-env和nodemon,这里解释一下:

启动服务看一下吧

执行以下命令开启开发模式。

>npmrundev

可以看到我们成功的启动服务了,监听到了3000端口。

在服务中,我们搭载了socket服务,那要怎么测试是否有效呢?

所以我们现在简单的搭建一下前端吧。

Webpack搭建前端文件

我们在开发前端的时候,用到模块化的话会开发更加丝滑一些,并且还有生产环境的打包压缩,这些都可以使用到Webpack。

我们的打包有两种不同的环境,一种是生产环境,一种是开发环境,所以我们需要两个webpack的配置文件。

当然傻傻的直接写两个就有点憨憨了,我们将其中重复的内容给解构出来。

我们在根目录下创建webpack.common.js、webpack.dev.js、webpack.prod.js三个文件。

此步骤的懒人安装模块命令:

npm install @babel/core @babel/preset-env babel-loader css-loader html-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin terser-webpack-plugin webpack webpack-dev-middleware webpack-merge webpack-cli \--save-dev

//webpack.common.js constpath=require('path'); constMiniCssExtractPlugin=require('mini-css-extract-plugin'); constHtmlWebpackPlugin=require('html-webpack-plugin'); module.exports={ entry:{ game:'./src/client/index.js', }, //将打包文件输出到dist文件夹 output:{ filename:'[name].[contenthash].js', path:path.resolve(__dirname,'dist'), }, module:{ rules:[ //使用babel解析js { test:/\.js$/, exclude:/node_modules/, use:{ loader:"babel-loader", options:{ presets:['@babel/preset-env'], }, }, }, //将js中的css抽出来 { test:/\.css$/, use:[ { loader:MiniCssExtractPlugin.loader, }, 'css-loader', ], }, ], }, plugins:[ newMiniCssExtractPlugin({ filename:'[name].[contenthash].css', }), //将处理后的js以及css置入html中 newHtmlWebpackPlugin({ filename:'index.html', template:'src/client/html/index.html', }), ], };

上面的代码已经可以处理css以及js文件了,接下来我们将它分配给development和production中,其中production将会压缩js和css以及html。

//webpack.dev.js const{merge}=require('webpack-merge') constcommon=require('./webpack.common') module.exports=merge(common,{ mode:'development' })

//webpack.prod.js const{merge}=require('webpack-merge') constcommon=require('./webpack.common') //压缩js的插件 constTerserJSPlugin=require('terser-webpack-plugin') //压缩css的插件 constOptimizeCssAssetsPlugin=require('optimize-css-assets-webpack-plugin') module.exports=merge(common,{ mode:'production', optimization:{ minimizer:[newTerserJSPlugin({}),newOptimizeCssAssetsPlugin({})] } })

上面已经定义好了三个不同的webpack文件,那么该怎么样使用它们呢?

首先开发模式,我们需要做到修改了代码就自动打包代码,那么代码如下:

//src/servers/server.js constwebpack=require('webpack') constwebpackDevMiddleware=require('webpack-dev-middleware') constwebpackConfig=require('../../webpack.dev') //前端静态文件 constapp=express(); app.use(express.static('public')) if(process.env.NODE_ENV==='development'){ //这里是开发模式 //这里使用了webpack-dev-middleware的中间件,作用就是代码改动就使用webpack.dev的配置进行打包文件 constcompiler=webpack(webpackConfig); app.use(webpackDevMiddleware(compiler)); }else{ //上线环境就只需要展示打包后的文件夹 app.use(express.static('dist')) }

接下来就在package.json中添加相对应的命令吧。

{ //... "scripts":{ "build":"webpack--configwebpack.prod.js", "start":"npmrunbuild&&cross-envNODE_ENV=productionnodemonsrc/servers/server.js" }, //... }

接下来,我们试试dev和start的效果吧。

可以看到使用npm run dev命令后不仅启动了服务还打包了前端文件。

再试试npm run start。

也可以看到先打包好了文件再启动了服务。

我们来看看打包后的文件。

测试Socket是否有效

先让我装一下前端的socket.io。

>npminstallsocket.io-client--save

然后编写一下前端文件的入口文件:

//src/client/index.js import{connect}from'./networking' Promise.all([ connect() ]).then(()=>{ }).catch(console.error)

可以看到上面代码我引入了另一个文件networking,我们来看一下:

//src/client/networking importiofrom'socket.io-client' //这里判断是否是https,如果是https就需要使用wss协议 constsocketProtocal=(window.location.protocol.includes('https')?'wss':'ws'); //这里就进行连接并且不重新连接,这样可以制作一个断开连接的功能 constsocket=io(`${socketProtocal}://${window.location.host}`,{reconnection:false}) constconnectPromise=newPromise(resolve=>{ socket.on('connect',()=>{ console.log('Connectedtoserver!'); resolve(); }) }) exportconstconnect=onGameOver=>{ connectPromise.then(()=>{ socket.on('disconnect',()=>{ console.log('Disconnectedfromserver.'); }) }) }

上面的代码就是连接socket,将会自动获取地址然后进行连接,通过Promise传给index.js,这样入口文件就可以知道什么时候连接成功了。

我们现在就去前端页面中看一下吧。

可以很清楚的看到,前后端都有连接成功的相关提示。

创建游戏对象

我们现在来定义一下游戏中的游戏对象吧。

首先游戏中将会有四种不同的游戏对象:

我们来一一将其实现吧。

首先他们都属于物体,所以我给他们都定义一个父类Item:

//src/servers/objects/item.js classItem{ constructor(data={}){ //id this.id=data.id; //位置 this.x=data.x; this.y=data.y; //大小 this.w=data.w; this.h=data.h; } //这里是物体每帧的运行状态 update(dt){ } //格式化数据以方便发送数据给前端 serializeForUpdate(){ return{ id:this.id, x:this.x, y:this.y, w:this.w, h:this.h } } } module.exports=Item;

上面这个类是所有游戏对象都要继承的类,它定义了游戏世界里每一个元素的基本属性。

接下来就是player、Prop、Bullet的定义了。

//src/servers/objects/player.js constItem=require('./item') constConstants=require('../../shared/constants') /** *玩家对象类 */ classPlayerextendsItem{ constructor(data){ super(data); this.username=data.username; this.hp=Constants.PLAYER.MAX_HP; this.speed=Constants.PLAYER.SPEED; //击败分值 this.score=0; //拥有的buffs this.buffs=[]; } update(dt){ } serializeForUpdate(){ return{ ...(super.serializeForUpdate()), username:this.username, hp:this.hp, buffs:this.buffs.map(item=>item.type) } } } module.exports=Player;

然后是道具以及子弹的定义。

//src/servers/objects/prop.js constItem=require('./item') /** *道具类 */ classPropextendsItem{ constructor(){ super(); } } module.exports=Prop;

//src/servers/objects/bullet.js constItem=require('./item') /** *子弹类 */ classBulletextendsItem{ constructor(){ super(); } } module.exports=Bullet

上面都是简单的定义,随着开发会逐渐添加内容。

添加事件发送

上面的代码虽然已经定义好了,但是还需要使用它,所以在这里我们来开发使用它们的方法。

在玩家输入名称加入游戏后,需要生成一个Player的游戏对象。

//src/servers/core/socket.js classSocket{ //... listen(socket){ console.log(`Playerconnected!SocketId:${socket.id}`); //加入游戏 socket.on(Constants.MSG_TYPES.JOIN_GAME,this.game.joinGame.bind(this.game,socket)); //断开游戏 socket.on('disconnect',this.game.disconnect.bind(this.game,socket)); } //... }

然后在game.js中添加相关逻辑。

//src/servers/core/game.js constPlayer=require('../objects/player') constConstants=require('../../shared/constants') classGame{ //... update(){ constnow=Date.now(); //现在的时间减去上次执行完毕的时间得到中间间隔的时间 constdt=(now-this.lastUpdateTime)/1000; this.lastUpdateTime=now; //更新玩家人物 Object.keys(this.players).map(playerID=>{ constplayer=this.players[playerID]; player.update(dt); }) if(this.shouldSendUpdate){ //发送数据 Object.keys(this.sockets).map(playerID=>{ constsocket=this.sockets[playerID]; constplayer=this.players[playerID]; socket.emit( Constants.MSG_TYPES.UPDATE, //处理游戏中的对象数据发送给前端 this.createUpdate(player) ) }) this.shouldSendUpdate=false; }else{ this.shouldSendUpdate=true; } } createUpdate(player){ //其他玩家 constotherPlayer=Object.values(this.players).filter( p=>p!==player ); return{ t:Date.now(), //自己 me:player.serializeForUpdate(), others:otherPlayer, //子弹 bullets:this.bullets.map(bullet=>bullet.serializeForUpdate()) } } //玩家加入游戏 joinGame(socket,username){ this.sockets[socket.id]=socket; //玩家位置随机生成 constx=(Math.random()*.5 .25)*Constants.MAP_SIZE; consty=(Math.random()*.5 .25)*Constants.MAP_SIZE; this.players[socket.id]=newPlayer({ id:socket.id, username, x,y, w:Constants.PLAYER.WIDTH, h:Constants.PLAYER.HEIGHT }) } disconnect(socket){ deletethis.sockets[socket.id]; deletethis.players[socket.id]; } } module.exports=Game;

这里我们开发了玩家的加入以及退出,还有Player对象的数据更新,以及游戏的数据发送。

现在后端服务已经有能力提供内容给前端了,接下来我们开始开发前端的界面吧。

前端界面开发

上面的内容让我们开发了一个拥有基本功能的后端服务。

接下来来开发前端的相关功能吧。

接收后端发送的数据

我们来看看后端发过来的数据是什么样的吧。

先在前端编写接收的方法。

//src/client/networking.js import{processGameUpdate}from"./state"; exportconstconnect=onGameOver=>{ connectPromise.then(()=>{ //游戏更新 socket.on(Constants.MSG_TYPES.UPDATE,processGameUpdate); socket.on('disconnect',()=>{ console.log('Disconnectedfromserver.'); }) }) } exportconstplay=username=>{ socket.emit(Constants.MSG_TYPES.JOIN_GAME,username); }

//src/client/state.js exportfunctionprocessGameUpdate(update){ console.log(update); }

//src/client/index.js import{connect,play}from'./networking' Promise.all([ connect() ]).then(()=>{ play('test'); }).catch(console.error)

上面的代码就可以让我们进入页面就直接加入游戏了,去页面看看效果吧。

image.png

原文链接: https://mp.weixin.qq.com/s/hoc5YXVRDDV_7jGmrO5Vfg

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved