I decided to release a new version of my old browser game, which for a couple of years was a success as an application in social networks. This time I set myself the goal of making it also in the form of an application for Windows (7-8-10) and place it in various stores. Of course, in the future you can make builds for MacOS and Linux.
The game code is written completely in pure javascript. For displaying 3D graphics, the three.js library is used as a link between the script and WebGL. However, it was the same in the old browser version. The most important thing in this project for me was the occasion to add my own library in parallel with the game, designed to complement three.js with convenient tools for working with scene objects, their animation and many other features. I then abandoned her for a long time. It is time to return to it.
My library contains convenient tools for adding and removing objects from the scene, changing the properties of individual parts of objects (meshes), frame-independent motion animation of 3D objects, the sky shader with the sky texture at night and much more. I will tell about some of them. Regarding the sky, I implemented its creation with one function, which takes a number of input parameters, initializes the shader, loads the clouds texture (if needed), and starts updating the sky with a specified iteration. ')
However, everything is a bit more complicated there - for periodically but rarely called functions, another construction actually works using setInterval (), into which events can be thrown at all at different intervals, but it will reduce all this to a common denominator and work out time the necessary events on the list. There you can also throw the sky update interval. But the movement of 3D game objects for greater smoothness has already been implemented through requestAnimationFrame () ...
So, since we are talking about heaven, we will start with it.
Sky
Adding a firmament to the scene is as follows.
First you need to add the standard three.js light to the stage with its maximum (initial) brightness values. The entire scene, with its objects, lights, and other attributes, so as not to clutter up the global space, will be stored in the apscene namespace.
As a result, a dynamic sky appears on the scene with a smooth change in the height of the sun and, accordingly, a change in the time of day.
It is not necessary to use all types of lighting on the stage. And it is not necessary to change all the parameters depending on the time of day. But, playing with their brightness, nevertheless, one can create a rather realistic picture of the change of day and night. You can name the parameters as you like, the main thing is to observe the object keys inside them as they are specified in three.js.
How it looks, you can watch the video from the demo scene:
This is another game. Just in it the horizon is not cluttered with various objects and therefore the work of this script is most clearly visible. But in the game about which the story goes, exactly the same approach is used. The high rate of flow of time here is set only for the purpose of demonstration, and so time flows, by itself, more slowly, with the same iteration step of the sky update. In this demo, by the way, a water shader is involved, also with variable parameters, depending on the height of the sun ... But I have not yet finalized it.
Performance
All this is very little demanding of the gland. Working in the Chrome browser, it loads the Xeon E5440 under the LGA775 (and with 4 gig of RAM) by 20%, and the core of the GT730 video card - by 45%. But this is purely due to the animation of the water. If we talk about the game, where there is no water, but there is a city, that's about this:
then at the time of the movement of the car in the city - 45% percent, video card 50%. In principle, with some drawdown of fps (seemingly up to about 30 frames per second), this works tolerably well even on a Pentium4 3GHz (1Gb RAM) and on a tablet on Intel Atom 1.3GHz (2Gb RAM).
All this hardware is extremely weak and other similar games on WebGL and on HTML5, even some 2D, they have a brazen effect on me, to the point that it becomes impossible to play them. As they say, write the games yourself, as you need, and play.
Scene
The 3D scene in three.js is a scene object and an array of its children are, in fact, all the 3D models loaded into the scene. In order not to prescribe a bootloader call for each model, I decided that the entire game scene would be defined in the form of a certain configuration, one large associative array locd: {} (such as location data), which would contain all the settings — lights, paths of preload textures and images for the interface, paths to all models that need to be loaded onto the stage, and more. In general, this is the complete scene configuration. It is set once in the js-file of the game and fed to my scene loader.
And in this object locd: {}, in particular, contains the paths to the individual 3D models that need to be loaded. The zero element is the common path, and then the relative paths for each object, such as:
It is assumed that all models are exported from a 3D editor to json format, that is, they have paths like path / myObj.json. This is followed by the scale (since the editor can be saved with an unsuitable scale for this game), the object's position in height (y), along the axes (x) and (z), followed by the rotation angle ® of the model along (y), a number of optional parameters and the name of the scene where to load the model - on the main scene (scene) or on the background scene (sceneb).
Yes, it was necessary to implement this not as a simple, but as an associative array. So incomprehensible is the order of the parameters and without documentation, or at least without the kind of function that accepts these parameters, you will not understand. I think in the future I will redo these lines into associative arrays. In the meantime, it looks like this:
These models are loaded onto the stage and placed in the coordinates specified in space. In principle, all models can be loaded as a single object, that is, exported from the editor as an entire game scene and loaded into coordinates (0; 0; 0). Then there will be only one line: landscape / ground - I have ground.json - this is the main part of the game world. But in this case it will be difficult to manipulate the individual objects of the scene, since you will need to first look in the browser console and remember which of the children of this huge ground what it is. And then refer to them by numbers. Therefore, it is better to load game models with separate objects. Then they can be accessed by names from an associative array, which will be automatically created specifically for this purpose.
The complete configuration of the game may look like this:
Yes, it is better to convert all these subarrays into associative arrays, but the order of the parameters in them is not clear ...
3D models
More from the interesting. Loading models. My library accepts 3D models with textures and automatically sets some parameters for their individual elements (meshes), depending on the names. The fact is that if for a model, for example, a shadow is set to cast, then each mesh that is included in it will cast it. It is not always necessary that the entire model cast a shadow completely or acquire any other properties that strongly affect performance. Therefore, if you turn on a flag indicating that it is necessary to treat each mesh individually, then during loading you can set which mesh will have this or that property, and which will not. Well, for example, there is absolutely no need for a shadow to cast a flat horizontal roof of the house or a lot of any small minor details of the model against the background of a large one. All the same, these shadows will not be visible to the player, and the power of the video processor to process them will be used.
To do this, in the graphical editor (Blender, Max, etc.) you can immediately set the names of meshes (in the object's name field) according to a certain rule. There must be an underscore (_). In the left part, conditional control characters should go, for example: d - doubleside (double-sided mesh, otherwise - one-sided), c (cast shadow) - casts a shadow, r (receive shadow) - accepts shadows. That is, for example, the name of the mesh of the pipe in the composition of the house can be - cr_tube. Many other letters are used. For example, “l” is a collider, that is, the wall of a house, having the name crl_wall01, will not allow the player to pass through itself, and will also cast and take a shadow. There is no need to make colliders, such as a roof or a door handle, and thus degrade performance. As you already understood, my library, when loading the model, parses the names of the meshes and gives them the corresponding properties on the scene. But for this you need to correctly name all the meshes before exporting the model from the 3D editor. This will significantly save performance.
All control flags for meshes inside the object:
col_ ... is a collider (collider). Such a mesh will be displayed simply as a transparent, invisible collider. In the editor, it can look like anything, only its shape is important. For example, it can be a parallelepiped around the whole model, if necessary, so that the player cannot pass through this model (building, large stone, etc.).
l_ ... is a collider object. Giving any mesh properties to the collider.
i_ ... - intersections. The mesh will be added to the intersections list, which can be used, for example, to click on it, that is, to add interactivity to the game.
j_ ... also intersections. Same as above, only the newer version - with an improved intersection search algorithm in the game and less resource consumption.
e_ ... - intersections for the doors of houses (entrance / exit). Eliminates working off intersections on other object mesh. It is used if homes need at some point to make only the doors interactive, excluding all other interactive elements. With fantasy, you can think of this and a lot of other applications.
c_ ... - cast shadows. Mesh casts a shadow.
r_ ... - receive shadows. The mesh takes shadows from all the other meshes that cast them.
d_ ... - double-sided. Visible on both sides, the texture is superimposed on both sides.
t_ ... - transparent (transparent) if the entire object is set to alphatest in three.js.
u_ ... - transparent (transparent), with a fixed density (0.4), if alphatest is not specified for the entire object in three.js.
g_ ... - glass (glass). Fixed transparency is set (0.2).
h_ ... - invisible (hidden). For parts of the object (meshes) that should be hidden when adding an object to the scene. Entered in the list of hidden.
v_ ... visible. All objects except those marked with “h” are already visible, but with the flag “v” they are entered into a separate list of visible ones for further hiding or other manipulations.
As a result, the mesh name may well be something like this: crltj_box1 (drops, accepts shadow, collider, transparent, interactive). And another mesh in the same model: cr_box2 (only casts and accepts shadows). Naturally, control characters can be set in any order. Thus, already from the editor, you can control the future display of parts of the object in the game, or rather, some of their properties, saving, at the same time, the computational power.
The essence of the game
The meaning, in fact, of the game about which the narration is to move around the perimeter of a square field and buy businesses. The field is made in the form of 3D streets. The economic model of the game is significantly different from the ones she likes. In my game, when someone opens a business, your expected profit drops. Conversely, when you open something, it increases. All calculations for profit and loss are made in the Tax Inspectorate on the Start field. You can also take a loan from a bank, trade in securities and crank a number of other cases. I have improved the behavior of AI compared to the old version. Redid almost all 3D models and textures of the game and optimized performance. Made more settings and much more.
Animation
To animate the movement of game objects, the simplest engine is used, which with a given frame rate (it is limited to 60th) changes any numerical parameters for a given time interval, producing, in addition, intermediate values ​​to the handler. And in the handler, for example, the model is displayed.
For example. We need to move object obj in space from position (10; 10; 50) to point (100; 300; 60). We set 3 parameters by specifying their initial and final values. The x coordinate will vary from 10 to 100, y - from 10 to 300 and z - from 50 to 60. And all this should happen, say, in 4 seconds.
The first line of 5 parameters: moveobj - animation name (any), 1- stream number (you can animate objects in parallel in an unlimited number of streams), 4000 - animation time 4 seconds, and - while an unused parameter that will be responsible for the transition logic in the future between animations within a single stream, userpar - any associative array that will be passed to the handler as a parameter, for example, with a pre-calculated radius, sines, cosines, and generally any values ​​predicted for this animation, in order not to be calculated about each iteration. Or with reference to the 3D object, which, in fact, will be animated.
Next comes an array with variable parameters. We understand that the first parameter is the change in the x coordinate, the second is the y, the third is the z. We register for each in lim1 and lim2, from what and to what value it will change. In sstart and sfin we specify the same values. Here you can specify the start, for example, from some other value, then the parameter will “scroll” in a circle from it and to it, passing lim2 and starting a new “turn” with lim1. Well, for example, this is necessary if we have an animation looped between some values ​​(lim1 and lim2), but we need to start it not from the beginning (that is, not with lim1), but from some intermediate value.
t: 0 just sets the fact that the animation for this parameter is performed 1 time, according to the total time (4000), as if stretching to it. If we specify another number, less than the main time, this parameter will be looped and will be repeated until the main animation time (4000) expires. This is convenient, for example, for setting a rotation to an object, when the angle must repeatedly cross the 360-degree line and reset to 0.
Next come 2 kollbek - the one that will be executed with each iteration and the one that will be executed once after the completion of the entire animation (exit point).
The first myPeriodicFun (this) callback, for example, can be:
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); }; },
That is, at each iteration of movement into this function, the parameter (self) is thrown, containing the calculated intermediate values ​​for all the specified animation parameters: self.par [0] .state, self.par [1] .state, self.par [2] .state. These are our x, y and z at the current time. For example, if the animation duration is 4 seconds, after 2 seconds, x will be (100-10) / 2 = 45. And so for all coordinates. Accordingly, in myPeriodicFun we simply display our object in these coordinates.If the browser starts to lag or just works slowly on this hardware, it is not scary: the total animation time will not change, only the frame rate will fall and the picture will turn into a slide show.
Why check f ((state! = State_old) ..., that is, whether the new calculated value is not equal to the old one (the state calculated in the previous iteration is remembered), well, for example, so that if some parameter changes less than one, do not redraw the entire object and do not waste the power of the system, and the animation engine gives integer numbers to state and state_old, which, say, can be interpreted as a step equal to a pixel. And if the object has not shifted relative to the previous position even at 1 pixel, there is no need to redraw it, since His position on the screen does not change.
In general, animation refers to a simple change in any number of parameters over a certain time with the output of their intermediate values ​​to a callback function. For example, you can add another 4th parameter that will be responsible for the angle of rotation of the object. And you can even shove the parameters of the set of objects into one animation, if they move somehow uniformly. You can put animations in different streams, then they will be processed in parallel. You can add (m3d.lib.anim.add ()) into one stream the whole sequence of animations, and they will be executed one after another. Moreover, each stream will have its own independent sequence. The main thing is to have enough power of the system and everything does not turn into a slide show.
PS The threads here are implemented by simply iterating through each iteration of all parallel animations and calculating for each of them the intermediate values ​​of all their parameters. Ie, there is no real multithreading in javascript.
The same “engine” can also be used to animate interface elements by setting them to change 2 coordinates on the screen plane and display these elements in callback functions based on the resulting intermediate values. What, in fact, done in my game.
Dynamic shadows
Displaying shadows throughout the whole scene turned out to be so wasteful that when turned on, fps fell several times. This is no good. We will display the shadows of objects in a kind of small square around the player. I have to say that this technique significantly increases the frame rate.
There is nothing difficult. The shadows of the three.js are dropped inside a certain shadow camera defined by the box (shadowCameraLeft, shadowCameraRight, shadowCameraTop, shadowCameraBottom). It can be stretched to the whole scene, and it can be made so that it follows the main camera and the shadows would be cast only around it. The only thing I added to this system is a step through which the shadows will be updated. There is absolutely no need to do this update every time the player twitches, since it loads the system with calculations. Let it overcome some minimum distance along any of the three axes so that the shadows will be updated.
When I initialize the 3D world, a contr object is created in my library, which at any time contains the coordinates of the camera. There you can also set the user parameter contr.cameraForShadowStep, which contains the camera position step, at which the position of the shadow camera changes.
If, say, the parallelepiped of the shadow camera has dimensions of 700x700x700, then contr.cameraForShadowStep can be set to, for example, 20. And when the player is shifted by 20 on any of the axes, the original position will be remembered again and the shadows around the player will be updated. The scale of the 3D world can be any, depending on the scale at which all the models were created in the 3D editor. And it is likely that instead of 700x700x700 and 20, you will need to use 7000x7000x7000 and 200. But this does not in any way change the essence.
By the way, when the sun moves across the sky, the shadows are updated independently of this system, since the direction of the shadows must change there. That is, they will be updated even if the player stands without moving. There the function of updating the shadows by the sky update period is stupidly called.
Point light system
The presence of more than a dozen point sources of light on the stage beats fps as much as dynamic shadows. And even makes it impossible to play on the old "Hemp". Moreover, it does not matter whether these sources are in the field of visibility of the player (the distance of drawing the world can be set) or at a considerable distance. If they are stupidly present on the stage, then everything works slowly. Therefore, I have provided options for the number of such sources (2, 4 or 8) under the name “Light of lanterns” in the game settings menu, which can be called up before loading the 3D world.
Thus, at night to turn on all the lights placed on the stage at the same time will not work. Above, in the description of the initialization scheme of the world, I gave arrays userPointLights and lightsPDynamicAr. In userPointLights, the coordinates of all the lantern lamps on the scene are specified by an array. And lightsPDynamicAr contains light settings for all 8 instances. Depending on the setting of the number of lamps, the library will take the first of them and add to the scene in the field of view of the player.
In fact, while the player is moving, a search takes place on the 2-8 lanterns closest to the player along the array of userPointLights lantern coordinates. And point sources of light move under them. In other words, 2-8 light bulbs follow the player, surrounding him. Moreover, this is also done not at every frame of fps, but with a predetermined step. There is absolutely no need to run the search function 60 times per second, especially if the player does not move - then let him burn the lights already found around him.
This is how it looks in motion (Xeon E5440, GeForce GT730):
Build distributive
Since I do not use any advanced development environment (except for advanced notebook), I wrote a bat-file, which calls Google Closure Compiler to obfuscate the code of each * .js file. And then nwjc.exe from the nw.js bundle is invoked there — to compile js into binaries (* .bin). I will give an example for one of the files:
java -jar D: \ webservers \ Closure \ 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: \ "Program Files" \ Web2Exe \ down \ nwjs-sdk-v0.35.5-win-ia32
D: \ "Program Files" \ Web2Exe \ down \ nwjs-sdk-v0.35.5-win -ia32 \ nwjc.exe D: \ webservers \ proj \ nwProjects \ bus \ game \ bus \ bus.js D: \ webservers \ proj \ nwProjects \ bus \ game \ bus \ bus.bin
del D: \ webservers \ proj \ nwProjects \ bus \ game \ bus \ bus.js
Next, I use a simple Web2Executable utility to create an exe file with an assembly under Windows. I chose 0.35.5 version nw.js, despite the fact that newer ones are available. I did not notice any effect from them, except for an increase in the size of the assembly.
The utility is able to download the selected version of nw.js itself into the specified folder. The output is assembly. The executable file of 35 megabytes contains, in fact, the game itself. Everything else is node-webkit. The locales folder contains files, obviously, with some resources in different languages. I poudalyal them and left only related to English. By the way, this did not prevent the launch of the Russian-language version of the game (in the game, the language switches between Russian and English). Why all these files then, I do not know. But without English, nothing starts.
The entire assembly eventually took 167 MB.
Then I packed everything into one executable distribution file using one of the free utilities designed for this purpose, and at the output I received a File Businessman3DSetup.exe with a capacity of 70.2 MB.
Publication
I sent the assembly to different app stores. Most of them are still moderating my game. At the moment, it has only been published by itch. I warn you at once, the game is paid, the price is $ 3. There are no purchases yet, but I have not yet been engaged in its promotion. The GOG refused to publish, citing the fact that the game is fairly simple and niche. I, in principle, agree. Epic Store, I think, will do the same.
The published version of the game is single player, with bots ranging from 1 to 5. Language - Russian and English. I intend to finish the network version. But while in thought - to release it in the form of the same application or in the form of a browser-based web version, available immediately in Windows, in Linux, in iOs and in MacOs, and generally everywhere where the browser supports WebGL. After all, in fact, webkit is a browser and the game works fine in it, in Firefox, in Edge and even in IE11 under Windows, but the latter is very slow.
findings
I guess I'm not ready to lay out my engine for general use, because it is not finished yet. I'm going to first write on it another game, just that one from the demo, about the ships, because in that game you can “run in” the work with the water shader. In addition, I plan to implement there the simplest physics engine. Yes, and you need to finish all the other possibilities, fix all the shortcomings. And it is better to do it in two games than in one, because some nuances are possible. In the meantime, my engine, in my opinion, is still too much sharpened for one game.
In addition, I am not at all sure that anyone needs all this. If you look soberly, then no one writes games in pure javascript. But I like it, because the games are pretty easy and fast for the browser. They load quickly, do not require a lot of RAM, and work quite quickly, compared to competitors on html5, even when compared to 2D. I think I will release more than one browser (and not only) game on all these developments.