我决定发布旧浏览器游戏的新版本,几年来,它作为社交网络上的应用程序一直很成功。 这次,我着手将其设计为Windows(7-8-10)应用程序,并将其放置在各个商店中。 当然,将来您可以为MacOS和Linux制作程序集。
游戏代码完全用纯JavaScript编写。 为了显示3D图形,Three.js库用作脚本和WebGL之间的链接。 但是,在旧版本的浏览器中就是这种情况。 对我来说,这个项目中最重要的事情是,与游戏同时添加自己的库的原因,该库旨在使用工具方便地对场景对象,其动画和许多其他功能进行补充,以补充Three.js。 然后我放弃了很长时间。 是时候回到她身边了。
我的库包含方便的工具,这些工具可用于添加和删除场景中的对象,更改对象各个部分(网格)的属性,与3D对象无关的帧频动画,夜间具有星空纹理的天空着色器等。 我将谈论其中的一些。 关于天空,我用一个函数实现了该函数,该函数接受多个输入参数,初始化着色器,加载云纹理(如果需要)并开始通过给定的迭代更新天空。
但是,那里的一切都有些复杂-对于定期的但很少调用的函数,实际上使用setInterval()可以进行另一种构造,在这种构造中,可以以不同的间隔抛出事件,并且将所有结果简化为一个公分母,并正确地进行计算。时间所需的事件在列表中。 在那里,您还可以抛出天空更新间隔。 但是,已经通过requestAnimationFrame()实现了3D游戏对象的移动以获得更高的平滑度...
因此,既然我们在谈论天空,那么我们将从它开始。
天空
将场景添加到场景的方法如下。
首先,您需要将具有最大(初始)亮度值的标准光源three.js添加到场景中。 整个场景及其对象,灯光和其他属性,以免混乱全局空间,将存储在后世命名空间中。
在那之后,您已经可以通过我的功能之一使用着色器,纹理(二十一点和...好吧,好的)来运行天空的动画:
m3d.graph.skydom.initWorld(
结果,动态天空出现在场景中,并且太阳高度发生了平滑变化,因此一天中的时间也发生了变化。
不必在舞台上使用所有类型的照明。 并且不必根据一天中的时间更改所有参数。 但是,通过发挥它们的亮度,您可以创建出昼夜变化的相当逼真的画面。 您可以根据需要命名参数,主要是观察在Three.js中指定的对象的键。
外观如何,您可以从演示场景中观看视频:
这是一个不同的游戏。 只是地平线没有杂乱无章的物体,因此此脚本的工作最为清晰可见。 但是在讨论故事的游戏中,使用了完全相同的方法。 此处设置时间的高速度只是出于演示的目的,因此时间当然会更慢地流过,迭代的同一步骤会更新穹顶。 顺便说一下,在此演示中,涉及到了一个着色器,该着色器也具有可变的参数,具体取决于太阳的高度。但是我还没有完成它。
性能表现
所有这些对铁的要求很少。 在Chrome浏览器中工作时,它可以将LGA775(并具有4 GB的RAM)下的Xeon E5440加载20%,将GT730显卡的核心加载45%。 但这纯粹是由于水的动画效果。 如果我们谈论没有水但有城市的游戏,则该游戏:
然后当时汽车在城市中移动-百分之四十五,视频卡百分之五十。 原则上,以一定的fps速度下降(每秒约30帧),即使在Pentium4 3GHz(1Gb RAM)和Intel Atom 1.3GHz(2Gb RAM)的平板电脑上也能很好地工作。
所有这些硬件都非常薄弱,在WebGL和HTML5上的其他类似游戏,甚至在某些2D上,它对我的速度实在是太慢了,甚至无法播放。 正如他们所说,根据需要自己编写游戏并玩。
场景
three.js中的3D场景是场景对象,其子数组实际上是加载到场景中的所有3D模型。 为了不为每个模型注册引导加载程序调用,我决定将整个游戏场景设置为某种配置,并放置一个大型关联数组:{}(例如位置数据),其中包含所有设置-灯光,预加载纹理的路径以及界面图片,应加载到舞台上的所有模型的路径等。 通常,这是场景的完整配置。 它在游戏的js文件中设置一次,然后输入到我的场景加载器中。
而且,此locd:{}对象特别包含指向需要加载的各个3D模型的路径。 零路径是公共路径,然后是每个对象的相对路径,例如:
['path/myObj', scale, y, x,z, r*Math.PI, 1, '', '', '', 1, ['','','',''], 'scene']
可以理解,所有模型都从3D编辑器导出为json格式,也就是说,它们具有路径,例如path / myObj.json。 其次是比例尺(因为可以使用不适合游戏的比例尺保存编辑器),对象在高度(y)上沿轴(x)和(z)的位置,然后在(y)中模型的旋转角度®,许多可选参数和在主场景(场景)或背景(场景)上加载模型的场景名称。
是的,有必要不以简单的形式来实现它,而是以关联数组的形式来实现。 因此,即使没有文档,或者至少没有采用这些参数的函数,参数的顺序也是难以理解的,您将无法理解。 我认为,将来我会在关联数组中重新制作这些行。 同时,它看起来像这样:
landobj: [ ['gamePath/'], [ ['landscape/ground', 9.455, 0, 0,0, 0*Math.PI, 1, '', '', '', 1, ['','','',''], 'scene'], ['landscape/plants', 9.455, 0, 0,0, 0*Math.PI,1, '', '', '', 1, ['','','',''], 'scene'], ['landscape/buildings/house01', 2, 0, -420,420, -0.75*Math.PI, 1, '', '', '', 1, ['','','',''], 'scene'], ... ] ],
这些模型被加载到舞台上,并放置在此处给定的空间坐标中。 原则上,所有模型都可以作为单个对象加载,即从编辑器导出为整个游戏场景并加载到坐标(0; 0; 0)中。 然后只有一行:风景/地面-我有ground.json-这是游戏世界的主要部分。 但是在这种情况下,将很难操作场景中的各个对象,因为您将需要先在浏览器控制台中查看并记住该广阔背景中的哪个子级。 然后通过电话联系他们。 因此,漫游游戏模型最好加载单独的对象。 然后可以从关联数组中按名称访问它们,为此将自动创建该数组。
游戏的完整配置可能看起来像这样:
locd:{
是的,最好将所有这些子数组重做为关联数组,否则参数中的顺序不清楚...
3D模型
另一个有趣的事情。 加载模型。 我的库接受带有纹理的3D模型,并根据名称自动为其个别元素(网格)设置一些参数。 事实是,例如,如果将模型设置为投射阴影,则它将由其合成中包含的每个网格物体投射。 不一定总是整个模型都完全蒙上阴影或获得任何会严重影响性能的属性。 因此,如果打开某个标志,表明有必要分别考虑每个网格,则在加载时可以确定哪个网格将具有该属性或哪个属性,而哪个网格将不具有该属性。 好吧,例如,绝对不需要通过房屋的水平屋顶或大型背景下模型的一些次要无关的细节来投射阴影。 同样,播放器将无法看到这些阴影,并且视频处理器功能将用于处理它们。
为此,在图形编辑器(Blender,Max等)中,您可以根据特定规则立即指定网格名称(在“对象名称”字段中)。 必须有一个下划线(_)。 条件控制字符应位于左侧,例如:d-双面(网格为双向,否则-单向),c(投射阴影)-投射阴影,r(接收阴影)-摄取阴影。 即,例如,房屋中的管网的名称可以是cr_tube。 也使用许多其他字母。 例如,“ l”是对撞机,也就是说,房子的墙壁名为crl_wall01,将不允许玩家通过它自己,也将投下阴影。 无需制造诸如屋顶或门把手的对撞机,从而降低性能。 正如您已经了解的那样,在加载模型时,我的库会解析网格的名称,并为它们提供场景中的相应属性。 但是为此,在从3D编辑器导出模型之前,必须正确命名所有网格。 这将大大节省性能。
对象内部网格物体的所有控制标志:
col_ ...是对撞机。 这样的网格将简单地显示为透明的不可见碰撞体。 在编辑器中,他可以看起来像任何东西,只有其形状很重要。 例如,如果玩家有必要不能穿越该模型(建筑物,大石头等),则可以在整个模型周围使用平行六面体。
l_ ...是可碰撞的对象。 给任何网格提供对撞机属性。
i_ ...-交叉路口。 网格将被添加到相交列表中,可用于例如单击它,即赋予游戏互动性。
j_ ...也相交。 与上述相同,只有较新的版本-带有用于在游戏中查找交叉点的改进算法,并减少了资源消耗。
e_ ...-房屋门的交叉点(入口/出口)。 排除与其他对象网格的相交。 如果有必要在某个时候使房屋仅与门互动,而其他所有互动元素除外,则使用此功能。 凭着想象力,您可以想到这一点以及大量其他用途。
c_ ...-投下阴影。 网格物体会投射阴影。
r_ ...-接收阴影。 网格接受所有其他投射阴影的网格的阴影。
d_ ...-双边(双面)。 在两侧可见,纹理在两侧重叠。
t_ ...-透明(透明),如果整个对象在three.js中设置为alphatest。
u_ ...-透明(透明),具有固定的密度(0.4),如果整个对象未在three.js中指定alphatest。
g _...-玻璃。 固定透明度设置为(0.2)。
h_ ...-不可见(隐藏)。 对于将对象添加到场景时应隐藏的对象部分(网格)。 在列表中输入隐藏的。
v_ ...可见。 除标有“ h”的对象外,所有对象均已可见,但带有标记“ v”的对象将输入一个单独的可见列表中,以进行进一步的隐藏或其他操作。
结果,网格的名称很可能是这样的:crltj_box1(投射,接受阴影,碰撞,透明,交互式)。 另一个网格是同一模型的一部分:cr_box2(仅投射和带阴影),自然,控制字符可以按任何顺序设置。 因此,从编辑器中,您可以控制游戏中对象的各个部分的未来显示,或者更确切地说,控制其某些属性,同时节省计算能力。
游戏的精髓
实际上,故事所涉及的游戏的含义是在方形区域的周围移动并购买企业。 该字段以3D街道的形式制成。 游戏的经济模式与同类游戏有很大的不同。 在我的游戏中,当某人开始经营业务时,您的预期利润会下降。 反之亦然,当您发现某些东西时,它就会增加。 所有损益计算均在“开始”字段的“税务检查”中进行。 您还可以从银行贷款,进行证券交易以及做许多其他事情。 与旧版本相比,我改善了AI行为。 重做几乎所有3D模型和游戏纹理并优化性能。 进行更多设置等等。
动画制作
为了动画化游戏对象的运动,使用了最简单的引擎,该引擎以给定的帧频(限制为60)在给定的时间间隔内更改任何数值参数,并将中间值传递给处理程序。 并且在处理程序中,例如,显示模型。
举个例子 我们需要将对象obj在空间中从位置(10; 10; 50)移至点(100; 300; 60)。 我们通过指示其初始值和最终值来设置3个参数。 x坐标将在10到100之间变化,y(在10到300之间变化,z在50到60之间变化)。所有这些都应该在4秒钟内发生。
m3d.lib.anim.add( 'moveobj', 1, 4000, 'and', {userpar1:111, obj:my3DObject}, [ {lim1:10, lim2:100, sstart:10, sfin:100, t:0}, {lim1:10, lim2:300, sstart:10, sfin:300, t:0}, {lim1:50, lim2:60, sstart:50, sfin:60, t:0} ], function(){ myPeriodicFun(this); }, function(){ myFinishFun(this); } ); m3d.lib.anim.play();
5个参数的第一行:moveobj-动画名称(任意),1-流编号(您可以在无限数量的流中并行设置对象的动画),4000-动画时间4秒,以及-直到未使用的参数为止,该参数将来将负责过渡逻辑在同一流中的动画之间,userpar是将作为参数传递给处理程序的任何关联数组,例如,具有计算出的半径,正弦,余弦值,并且通常为此动画计算出的任何值都将不计入 关于每次迭代的时间。 或参考一个3D对象,该对象实际上将进行动画处理。
接下来是具有可变参数的数组。 我们发现第一个参数是x坐标的变化,第二个参数是y,第三个参数是z。 我们为lim1和lim2中的每一个写东西,它将改变大小和大小。 在sstart和sfin中,我们指定相同的值。 例如,您可以在此处指定一个起始值,例如,从某个其他值开始,然后该参数将在其周围“滚动”一个值,绕过lim2并使用lim1开始新的“旋转”。 好吧,例如,如果我们的动画在某些值(lim1和lim2)之间循环,则这是必要的,但是我们不需要从头开始(即,不是从lim1)而是从某个中间值开始。
t:0只是设置此参数的动画按照总时间(4000)执行1次,就好像拉伸了它一样。 如果我们设置的另一个数字小于主要时间,则此参数将循环播放并重复直到主要动画时间(4000)。 例如,当角度必须重复越过360度线并重置为0时,设置对象旋转很方便。
接下来是2个回调-将在每次迭代时执行一次,并在整个动画完成(退出点)后执行一次。
例如,第一个回调myPeriodicFun(this)可以像这样:
myPeriodicFun:function(self) { var state=self.par[0].state, state_old=self.par[0].state_old; var state2=self.par[1].state, state_old2=self.par[1].state_old; var state3=self.par[2].state, state_old3=self.par[2].state_old; if ((state!=state_old)||(state2!=state_old2)||(state3!=state_old3)) { var obj=self.userpar.obj; obj.position.x=state; obj.position.y=state2; obj.position.z=state3; ap.cameraFollowObj(obj); }; },
也就是说,在每次运动迭代时,都会向此函数中抛出一个参数(自我),其中包含所有给定动画参数的计算中间值:self.par [0] .state,self.par [1] .state,self.par [2]状态 这是我们当前的x,y和z。 例如,如果动画持续4秒钟,则2秒钟后x等于(100-10)/ 2 =45。因此,对于所有坐标。 因此,在myPeriodicFun中,我们仅在这些坐标中显示对象。
如果浏览器开始滞后或仅在该硬件上运行缓慢,这并不令人害怕:动画总时间不会改变,只会降低帧频,图片会变成幻灯片。为什么要检查f((state!= State_old)...,也就是说,例如,新计算的值是否等于旧值(state_old记住上一次迭代中的计算值),所以,如果某些参数发生了变化小于1,则不要重绘整个对象,也不用花费系统的力量,动画引擎会在state和state_old中生成整数,例如可以解释为等于像素的步长,并且如果对象没有相对于先前位置移动即使是1像素,也不需要重绘,因为e屏幕上的位置不变。通常,动画被理解为在一定时间内将其中间值传递给回调函数而对任意数量的参数进行的简单更改。例如,您可以添加另一个第4个参数,该参数负责对象的旋转角度。如果对象以某种方式均匀移动,通常可以将许多对象的参数填充为一个动画。您可以将动画放在不同的流中,然后将它们并行处理。您可以将(m3d.lib.anim.add())完整的动画序列添加到单个线程,它们将一个接一个地执行。而且,每个线程将具有其自己独立的序列。最主要的是,该系统具有足够的功能,并且所有内容都不会变成幻灯片放映。PS。这里的流是通过在所有并行动画的每次迭代中顺序枚举并为它们中的每个参数计算所有参数的中间值来简单实现的。也就是说,javascript中没有真正的多线程。通过将界面元素设置为在屏幕平面上更改2个坐标,并根据获取的中间值在回调函数中显示这些元素,还可以使用相同的“引擎”为界面元素设置动画。实际上,这是在我的游戏中完成的。动态阴影
在整个场景中显示阴影非常浪费,以至于在打开它们时,fps下降了好几次。不好 我们将在播放器周围的某个小方块中显示物体的阴影。我马上要说,这种技术会大大提高帧速率。这里没什么复杂的。 three.js的阴影投射在由框指定的某个阴影摄像机(shadowCameraLeft,shadowCameraRight,shadowCameraTop,shadowCameraBottom)内。可以将其拉伸到整个场景,也可以将其制作为使其跟随主摄像机,并且仅在其周围投射阴影。我添加到该系统的唯一一件事就是更新阴影的步骤。绝对不需要每次玩家抽动时进行此更新,因为这会给系统加载计算。让他克服沿三个轴中任一轴的最小距离,以便更新阴影。在我的媒体库中,初始化3D世界时,会创建一个contr对象,该对象随时包含相机的坐标。您还可以在其中设置用户参数contr.cameraForShadowStep,该参数包含摄像机位置变化的步骤,影子摄像机的位置在该位置发生变化。例如,如果阴影摄像机的平行六面体尺寸为700x700x700,则可以将contr.cameraForShadowStep设置为20。并且,当播放器在任意轴上移动20时,初始位置将被再次记住,并且播放器周围的阴影将被更新。 3D世界的规模可以是任意的,具体取决于在3D编辑器中创建所有模型的规模。您可能需要使用7000x7000x7000和200,而不是700x700x700和20。但这不会以任何方式改变本质。顺便说一下,当太阳在天空中移动时,阴影会独立于此系统进行更新,因为阴影的方向应该在那里更改。即,即使玩家站着不动,它们也将被更新。在那里,直言不讳地称为根据更新天空的周期来更新阴影的功能。点光源系统
舞台上存在十多个点光源,其fps和动态阴影一样强。甚至无法播放旧的“大麻”。此外,这些来源是否在玩家的视野中(可以设置世界渲染的范围)还是相距很远都没有关系。如果他们愚蠢地出现在舞台上,那么一切都会缓慢进行。因此,我在游戏的设置菜单中提供了该菜单,可在加载3D世界之前调用该菜单,并提供称为“灯笼之光”的此类来源(2、4或8)的选项。因此,在夜间,无法同时打开放置在舞台上的所有灯光。上面,在世界初始化方案的描述中,我介绍了userPointLights和lightsPDynamicAr数组。在userPointLights中,舞台上所有灯的坐标以阵列形式设置。而且lightsPDynamicAr包含所有8个实例的灯光设置。根据灯光数量的设置,媒体库将采用第一个灯光,并将其添加到播放器视场中的场景中。实际上,在玩家移动时,将通过userPointLights灯笼的坐标数组搜索最靠近玩家的2-8个灯笼。点光源在其下方移动。换句话说,2-8个照明灯跟随着玩家,围绕着他。此外,这并非对fps的每一帧都进行,而是对给定的步骤进行。绝对不需要每秒60次启动搜索功能,尤其是如果播放器不移动时-让已发现他周围的灯点亮。这是运动的样子(至强E5440,GeForce GT730):发行版本
由于我不使用任何复杂的开发环境(高级记事本除外),因此我编写了一个bat文件,其中调用了Google Closure Compiler来混淆每个* .js文件的代码。然后从其中调用nw.js包中的nwjc.exe,将js编译为二进制文件(* .bin)。我将举例说明其中一个文件:java -jar D:\ webservers \ closures \ compiler.jar --js D:\ webservers \ proj \ m3d \ www \ game \ bus \ bus.js --js_output_file D:\ webservers \ proj \ nwProjects \ bus \ game \ bus \ bus.js
cd D:\“程序文件” \ Web2Exe \ down \ nwjs-sdk-v0.35.5-win-ia32
D:\“程序文件” \ Web2Exe \ down \ nwjs-sdk-v0.35.5-win -ia32 \ nwjc.exe D:\ web服务器\ proj \ nwProjects \ bus \ game \ bus \ bus.js D:\ webservers \ proj \ nwProjects \ bus \ game \ bus \ bus \ bus.bin
del D:\ webservers \ proj \ nwProjects \公交车\游戏\公交车\ bus.js
接下来,我使用简单的Web2Executable实用程序在Windows下创建带有程序集的exe文件。我选择了nw.js版本0.35.5,尽管也有更新的版本。除了增加程序集的大小之外,我没有注意到它们的任何影响。该实用程序能够将选定版本的nw.js下载到指定的文件夹本身。输出是程序集。实际上,这个35兆字节的可执行文件包含游戏本身。其他所有内容都是node-webkit。很明显,locales文件夹包含文件,其中包含一些使用不同语言的资源。我删除了它们,只留下了与英语相关的内容。顺便说一下,这并没有阻止游戏的俄语版本的发布(在游戏中,语言在俄语和英语之间切换)。为什么不知道所有这些文件呢?但是没有英语,一切都不会开始。整个程序集最终占用了167 MB。然后,我使用为此目的设计的免费实用程序之一将所有内容打包到一个可执行的分发文件中,输出为70.2 MB Businessman3DSetup.exe。过帐
我将程序集发送到其他应用商店。他们中的大多数人仍在审核我的游戏。目前,只有itch出版过。我立即警告您,游戏已付款,价格为3美元。还没有购买,但是我还没有从事它的促销。 GOG拒绝发布该游戏,理由是该游戏非常简单且利基。我原则上同意。我认为Epic Store也会这样做。该游戏的发行版本为单用户,机器人数量为1-5。语言-俄语和英语。我打算完成网络版本。但仍在考虑-以相同应用程序的形式或以Web浏览器版本的形式发布它,可立即在Windows,Linux,iO和MacO上以及通常在支持WebGL的任何位置上使用。实际上,实际上,webkit是一种浏览器,并且游戏可以在其中运行良好,在Firefox,Edge,甚至Windows下的IE11中都可以运行,尽管在后者中它的运行速度非常慢。结论
我想我还没有准备好将我的引擎布置为通用版本,因为它还没有完成。我将首先在上面编写另一个游戏,只是有关船舶的演示视频中的一个,因为在该游戏上,您可以“运行”水着色器。另外,我计划在那里实现一个简单的物理引擎。是的,您仍然需要完成所有其他功能,修复所有缺陷。而且最好在两局游戏中胜过在一场游戏中做到这一点,因为可能会有些细微差别。同时,我认为我的引擎对于一款游戏来说仍然过于尖锐。另外,我完全不确定任何人都需要这一切。如果您清醒地看,没人会用纯JavaScript编写游戏。但我喜欢它,因为这些游戏对于浏览器而言非常简单快捷。与html5中的竞争对手相比,它们加载速度很快,不需要太多的RAM,并且工作速度非常快,即使与2D相比也是如此。我想我将在所有这些开发中发布多个(不仅是)浏览器游戏。