海,海盗-浏览器中的3D在线游戏

向Habr用户和普通读者致以问候。这是开发基于浏览器的多人在线游戏的故事,该游戏具有低多边形3D图形和简单2D物理原理。



后面有很多基于浏览器的2D迷你游戏,但是这样的项目对我来说是新的。在gamedev中,解决您尚未遇到的问题可能会非常令人兴奋和有趣。最主要的是不要在有欲望和动力的时候卡住磨削零件并开始工作,所以不要浪费时间开始开发!





简而言之



生存格斗是目前唯一的游戏模式。从2艘战舰到6艘战舰没有重生,最后一名幸存的玩家被视为获胜者,并获得x3点和金币。



街机控件:按钮W,A,D或箭头移动,空格键向敌舰开火。您不需要瞄准,您不能错过,伤害取决于射击的随机性和角度。更大的伤害伴随着“目标正确”勋章。



我们通过在24小时和7天内(在莫斯科时间00:00重置)在玩家评分中排名第一,并完成日常任务(依次发出一日三项)来赢得金牌。也有战斗用的黄金,但数量较少。



消费黄金在您的船上设置黑帆24小时。计划增加唤醒海妖的能力的计划,海妖会带走任何敌人的船底,其巨大的触角:)



PVP或扎萨尔· 胆小鬼?我想在选择海盗主题之前就实现的功能是,只需单击几次即可与朋友打架。无需注册和不必要的手势,您可以向您的朋友发送邀请链接,然后等到他们使用该链接进入游戏:当有人跟随该链接时,会自动创建一个可以为所有人打开的私人房间,前提是该链接的“作者”未启动另一个战斗。



技术栈



Three.js是在浏览器中使用3D的最受欢迎的库之一,提供了很好的文档和许多示例。另外,我之前使用过Three.js-选择很明显。



缺少游戏引擎的原因是缺乏相关经验,并且想要学习任何东西,否则一切都无法正常进行:)



Node.js,因为它简单,快速且方便,尽管我没有直接使用Node.js的经验。我认为Java是替代方法,它进行了一些本地实验,包括使用Web套接字,但不敢发现是否很难在VPS上运行Java。另一个选择-Go,它的语法使我感到沮丧-在其研究中尚未取得进展。



对于Web套接字,请使用Node.js中的ws模块。



PHP和MySQL不太明显的选择,但是标准仍然是一样的-快速,轻松,因为这些技术都有经验。



事实是这样的:







主要是需要PHP来向客户端提供网页以及处理罕见的AJAX请求,但是在大多数情况下,客户端仍通过Web套接字与Node.js上的游戏服务器通信。



我根本不想将游戏服务器链接到数据库,所以一切都通过PHP进行。我认为这里有优点,尽管我不确定它们是否有意义。例如,由于所需格式的现成数据进入Node.js,因此Node.js不会浪费时间处理和数据库中的其他查询,而是处理更重要的事情-它“消化”了玩家的行为并改变了房间内游戏世界的状态。



模特第一



开发始于一个简单且最重要的事情-游戏世界的特定模型,从服务器的角度描述了海上战斗。普通画布2D是在屏幕上示意性显示模型的理想选择。







最初,我设置了正常的“ verlet”物理学,并考虑了相对于船体方向不同方向的船舶运动阻力。但由于担心服务器性能,我用最简单的物理方法代替了普通的物理方法,在这种情况下,船的轮廓仅保留在视觉上,但实际上船是圆形物体,甚至没有惯性。代替惯性,前进加速度受到限制。



利用船舶方向和射击方向的矢量,将射击和打击简化为简单的操作。这里没有贝壳。如果考虑到目标的距离,归一化向量的点积适合可接受的值,那么如果玩家按下按钮,就会有击球的机会。



用于渲染游戏世界模型,处理飞船和射击运动的客户端JavaScript,我几乎没有改变地移植到Node.js服务器。



游戏服务器



Node.js WebSocket服务器仅包含3个脚本:



  • main.js-用于接收来自玩家的WS消息,创建房间并使该机器的齿轮旋转的主脚本
  • room.js-负责房间内游戏玩法的脚本:更新游戏世界,将更新发送给房间内的玩家
  • funcs.js-包括用于处理向量的类,几个辅助函数以及实现双链表的类


随着开发的进行,添加了新类-几乎所有新类都与游戏玩法直接相关,并最终出现在room.js文件中。有时,单独使用类(在单独的文件中)很方便,但是只要没有太多的类,将所有选项合为一体也不错(方便向上滚动并记住另一个类的方法采用的参数)。



游戏服务器类的当前列表:



  • WaitRoom-等待玩家开始战斗的房间,它有自己的tick方法,当一半以上的玩家准备战斗时,它会发送更新并开始创建游戏室
  • Room — , : /, ,
  • Player — «» :
  • Ship — : , , ,
  • PhysicsEngine — ,
  • PhysicsBody


Room
let upd = {p: [], t: this.gamet};
let t = Date.now();
let dt = t - this.lt;
let nalive = 0;

for (let i in this.players) {
	this.players[i].tick(t, dt);
}

this.physics.run(dt);

for (let i in this.players) {
	upd.p.push(this.players[i].getUpd());
}

this.chronology.addLast(clone(upd));
if (this.chronology.n > 30) this.chronology.remFirst();

let updjson = JSON.stringify(upd);

for (let i in this.players) {
	let pl = this.players[i];
	if (pl.ship.health > 0) nalive++;
	if (pl.deadLeave) continue;
	pl.cl.ws.send(updjson);
}

this.lt = t;
this.gamet += dt;

if (nalive <= 1) return false;
return true;




除了课程外,还有诸如获取用户数据,更新日常任务,获得奖励,购买皮肤等功能。这些函数基本上将https请求发送到PHP,PHP执行一个或多个MySQL查询并返回结果。



网络延迟



网络延迟补偿是在线游戏开发的重要组成部分。关于这个主题,我已经在哈布雷(Habré)上重复阅读了一系列文章。在帆船战中,滞后补偿可能很简单,但是您仍然必须做出妥协。



插值始终在客户端上执行-计算两个时间之间的游戏世界状态,该数据已经获得。时间间隔很小,可减少突然跳动的可能性,并且由于网络延迟大且缺少新数据,内插被外推替代。推算得出的结果不是很正确,但是对于处理器来说便宜,并且不依赖于如何在服务器上实现船舶移动,当然,有时可以节省情况。



解决滞后问题时,很大程度上取决于游戏及其进度。我牺牲了对玩家动作的快速响应,以支持平滑的动画效果以及图片在特定时间点与游戏世界状态的准确对应。唯一的例外是,按下按钮即可立即播放大炮齐射。其余的可以归因于宇宙定律和船员多余的朗姆酒:)



前端



不幸的是,没有明确的类和方法的结构或层次结构。所有JS都分成具有各自功能的对象,从某种意义上说,它们是相等的。我以前的几乎所有项目都比这个项目更具逻辑性。部分原因是因为第一个目标是在服务器和网络交互上调试游戏世界模型,而不关注游戏的界面和视觉组件。到了添加3D的时候,我从字面上将其添加到现有的测试版本中,大致来说,我用完全相同但3D的2D drawShip函数代替了它,尽管以友好的方式值得修改整个结构并为将来的更改奠定基础。



3D船



Three.js支持使用各种格式的现成3D模型。我为自己选择了GLTF / GLB格式,可以在其中嵌入纹理和动画,即 开发人员应该不会怀疑“是否已加载所有纹理?”



我以前从未处理过3D编辑器。合理的步骤是与自由交易的专家联系,以创建带有大炮齐射的嵌入式动画的帆船3D模型的任务。但是我无法忍受自己完成的专家模型的微小更改,最终导致了我在Blender中从头开始创建模型的事实。要创建几乎没有纹理的低多边形模型非常简单,如果没有专家的现成模型来在3D编辑器中研究特定任务的需求(至少在道德上是:),则很难。







着色器给着色器之神



我需要着色器的主要原因是能够在渲染期间操纵视频卡上对象的几何形状,这具有良好的性能。Three.js不仅允许您创建自己的着色器,还可以承担一些工作。



我在创建粒子系统来动画化对船舶,动态水面或静态海床的损坏时使用的机制或方法是相同的:特殊的ShaderMaterial提供了使用其着色器(其GLSL代码)的简化接口,BufferGeometry允许您从任意数据创建几何...



一个空的空白,这是一种代码结构,可方便我以类似的方式复制,补充和修改以创建3D对象:



显示代码
let vs = `
	attribute vec4 color;
	varying vec4 vColor;

	void main(){
		vColor = color;
		gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		// gl_PointSize = 5.0; // for particles
	}
`;
let fs = `
	uniform float opacity;
	varying vec4 vColor;

	void main() {
		gl_FragColor = vec4(vColor.xyz, vColor.w * opacity);
	}
`;

let material = new THREE.ShaderMaterial( {
	uniforms: {
		opacity: {value: 0.5}
	},
	vertexShader: vs,
	fragmentShader: fs,
	transparent: true
});

let geometry = new THREE.BufferGeometry();

//let indices = [];
let vertices = [];
let colors = [];

/* ... */

//geometry.setIndex( indices );
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 4 ) );

let mesh = new THREE.Mesh(geometry, material);




船舶损坏



船舶损坏动画是移动的粒子,它们会更改其大小和颜色,其行为取决于其属性和GLSL着色器代码。粒子(几何形状和材料)的生成是预先发生的,然后为每艘船创建自己的损坏粒子实例(网格)(几何形状对所有对象都是通用的,因此会克隆材料)。粒子属性有很多,但是创建的着色器同时实现了大型的缓慢移动的尘埃云,快速飞扬的碎片和火焰粒子,其活动取决于船舶的损坏程度。









海洋也使用ShaderMaterial实现。每个顶点沿着正弦曲线在所有3个方向上移动,从而形成随机波。该属性定义每个运动方向的振幅和正弦波的相位。



为了使水上的颜色多样化并使游戏更加有趣和令人愉悦,决定增加底部和孤岛。底部颜色取决于高度/深度,并在水表面发光,从而形成深色和浅色区域。



海床是从一个高度图创建的,该高度图分为两个阶段:首先,在图形编辑器中创建无岛的底部(在我的情况下,工具渲染->云和高斯模糊),然后使用JSFiddle上的Canvas JS以随机顺序添加岛画一个圆圈和模糊。有些岛屿很低,通过它们你可以向对手射击,而另一些则有一定的高度,射击不能穿过它们。除了高度图本身之外,在输出处,我还以json格式接收有关服务器上物理的岛(其位置和大小)的数据。







下一步是什么?



游戏开发有很多计划。主要的是新的游戏模式。较小的-考虑到WebGL和JS的性能限制,提出水上的阴影/反射。我已经提到了唤醒Kraken的机会:)尚未根据玩家积累的经验将玩家统一到房间中。一个明显但不是太高优先级的改进是创建几张海底和岛屿地图,并随机选择其中一张进行新的战斗。



您可以通过将场景重复“绘制到内存中”,然后将所有数据合并到一张图片中来创建很多视觉效果(实际上,可以将其称为后处理),但是我的手并不会因此增加客户端的负担,因为客户端仍然是浏览器而不是本地应用。也许有一天,我将决定这一步骤。



现在,我还很难回答一些问题:廉价的虚拟服务器可以承受多少在线玩家,是否有可能至少收集一定数量的感兴趣的玩家以及如何做到这一点。



复活节彩蛋



谁不喜欢记住那激动不已的旧电脑游戏?到目前为止,我喜欢一遍又一遍地玩游戏Corsairs 2(又名海狗2)。我忍不住为我的游戏添加了一个秘密,并明确地和间接地使人联想到“海盗2”。我不会透露所有的卡片,但会给我一个提示:我的复活节彩蛋是您在探索大海时可以找到的特定物体(您无需远航无尽的大海,该物体在合理范围内,但找到它的可能性并不高)。复活节彩蛋完全修复了受损的船。



发生了什么



分钟视频(通过2台设备进行测试):





链接到游戏:https : //sailfire.pw



还有一个联系表格,消息以电报的形式发送给我:https : //sailfire.pw/feedback/

那些希望了解最新消息和更新的链接:VK Public电报频道



All Articles