📜 ⬆️ ⬇️

Creating a game on Blend4Web. The beginnings of intelligence

Even the most primitive game character should have at least some "brains". Fish a priori do not shine with intelligence, but something they still need to be able to do - move, “watch”, run away or attack. They are not required to seek shelter or “wrinkle the forehead” for a smart response phrase. It looks simple, but is it easy to do?

The conversation will be about the implementation of AI by JavaScript and Blend4Web. The tasks set, the methods of their solution or the forced paths of a detour - all this is illustrated by the example of a live, game project.

Theoretical reasoning


I'll start with the entry. The game being developed is a horizontal scroller where the main character moves in the same direction (from left to right). As the hero serves the fish, resist the rest of the sea creatures. And the player does not affect the hero, but only helps him in solving the existing problems. Therefore, all the characters must have a little bit of reason to play was interesting. However, not much is required of them. I even note that the enemies are much smarter than the main character. Actually, what to take with a simple goldfish! This article is dedicated not to her, but to the humble and hungry inhabitants of the depths.

How does life begin in the game? Of course, with spawn! It seems that it may be easier to create the necessary number of start markers in the editor in advance and generate enemies at these points. However, at this stage, you can get poor replayability and a significant drop in performance. Who is interested in replaying the level, knowing in advance that a shark will jump out from there, and a moray eel will crawl out from behind the stone. It will help the generation of enemies in arbitrarily selected places (points). And if you add a simple algorithm for selecting characters and their number in accordance with the terrain, it will become much more interesting.
')
Much more complicated is the case with performance. In fact, the game is a continuously moving conveyor with a large number of active objects. On one screen there can be several enemies, just passing fish, other elements interacting with the environment. If you roughly imagine the length of a level, then the number of such objects can exceed hundreds. The avalanche miscalculation of physics, animation, logic can result in serious brakes and one of the solutions is a temporary suspension of the activities of unnecessary objects.



What is in the figure is my performance decision. As you can see, the playing field is divided into zones. At the start of the game, all objects are loaded into memory, scattered around the spawns and “frozen”. Depending on the location of the main character, there is an activation of objects of a particular sector. For example, in the figure, the main character is depicted in the zero zone. The objects of compartments 2 and 3 are activated accordingly. Then the hero swims into sector 1 - the number four is connected, etc. Activation of two zones at the same time, due to the need to give units time, at least for sailing from the places of generation. So does the shutdown. Here, however, a little smarter, because you need to consider that predators in a fit of excitement can cross the boundaries of their zone.

Those circles that are visible in the figure are places for spawns. In reality, they are located on the stage depending on the location, current tasks, etc. At first there was an idea to use these coordinates as points for constructing the trajectory of objects, but later I abandoned it, letting the creatures float free. Thereby, bringing even more surprises in the gameplay. If we take into account that during spawn, not only places are randomly indicated, but also the initial rotation of the object, then the trajectory of movement becomes unpredictable.

The main logic of character decisions is based on two events: the results of scanning the space in front of the object and the circular search. The first option is used for more or less meaningful movement. A beam released a certain distance in front of an object returns with some information. Imagine that there is a rock ahead. Depending on the distance to it, the character decides whether to swim further or turn. Moreover, the rotation is performed until the front has a clear space.

The second option is needed for more global action. Thus, the main character is detected in the attention zone. Accordingly, the character rushes to him in order to attack. If he himself is in danger, then the enemy flees away in the opposite direction.

This whole system of random movement looks very unstable. Suppose a situation may arise that a unit moves in the direction opposite to the hero or, on the contrary, stubbornly overtakes it. If he falls into the invisibility zone, then he will simply “freeze” until better times.

As for aggressive characters, they can only attack within a certain time. In the future, they “lose” interest and swim in any direction.

To summarize, the random generation of characters, their quantity and quality, the abandonment of rigid trajectories of movements, allows you to create a fairly unpredictable gameplay.

Practical approach


Theory is a good thing. However, often in practice it turns out to be completely not so cloudless, and many seemingly correct decisions require significant adjustments. Much depends on the functionality of the API engine and, of course, the professionalism of the programmer. Alas, I can’t boast the last, so many thanks to those Blend4Web developers for listening to my sometimes strange questions and helping as much as possible.

So, the used engine is Blend4Web, the programming language JavaScript, the target platform is the web. This is not my first article on the creation of the game. Check out previous materials if something is unclear (list in order of publication time):



By tradition, for key objects in the scene I use separate handler scripts. Therefore, the game_fish.js file was created for fish, and the global steel constants are stored in the game_config.js file.

AI processing is carried out independently of the rest of the code and, in fact, requires only initialization - a request to generate objects. There are several corresponding lines in the main game_app.js file:
… var m_fish = require("game_fish"); var m_game_config = require("game_config"); ... var number = 10; var type_fish = m_game_config.FISH1; m_fish.new_fishes(number, type_fish); ... 

With the help of m_fish.new_fishes, a request is made to create fish of a certain type ( type_fish variable) and the required number ( number ). Actually, nothing more is required from game_app, so we are going to the topic of conversation.

So, first a call is made to the function new_fishes, responsible for launching the mechanism for generating fish:
 var _type_fish; var _elapsed_sensor; var APP_ASSETS_PATH = m_cfg.get_std_assets_path() + "waterhunter/"; exports.new_fishes = function(number, type_fish) { _type_fish = type_fish; _elapsed_sensor = m_ctl.create_elapsed_sensor(); //load fish var i; for (i = 0; i < number; i++) { m_data.load(APP_ASSETS_PATH +type_fish, fish_loaded_cb,null,true,true); } } 

The first three lines describe global variables, and then a simple loop is executed based on the required number of instances ( number ).

The constant APP_ASSETS_PATH contains the path to game resources (json, media, etc.), which is used by the m_data.load function to load the fish model. You can learn more about the intricacies of working with load from the last lesson . I will add that the object is disabled after loading and becomes invisible.

In general, for multiple identical objects, it is common practice to copy (instancing) an already loaded instance. More recently, the corresponding functions have appeared in the Blend4Web API. But they are limited to simple copying of geometry and do not allow working with a more complex model hierarchy. This forces the use of the load function. Although it is well placed to manage the boot process, it is rather slow. For the sake of interest, I tried to load 50 copies of fish with it - I had to wait about 7 seconds, which is very bad for dynamically created objects.

At this stage of development, this is not critical, because the generation of objects can be hidden under the load level splash. But for a dynamic scene it is better to use a pool of objects.

The following function m_ctl.create_elapsed_sensor () looks very interesting. With its help, a special sensor object is created that generates an event with a certain periodicity and is capable of producing the time between the render of the current frame and the previous one. Simply put, it is useful for moving or rotating an object at the same speed, regardless of the power of the system. But, to work with him, you must first “subscribe” to this event. About this a little further.

When the file upload is completed, the function fish_loaded_cb is called . It is in it that further work continues.
 function fish_loaded_cb (data_id) { //    4 var spawn_number = Math.floor(Math.random() * (5 - 1)) + 1; //   - var spawn = m_scenes.get_object_by_name (m_game_config.SPAWN_FISH[spawn_number-1],0); //  spawn var spawn_coord = new Float32Array(3); spawn_coord = m_trans.get_translation (spawn, spawn_coord); //    var obj_fish = m_scenes.get_object_by_name ("fish", data_id); //    spawn m_trans.set_translation_v (obj_fish,spawn_coord); ... 

This bunch of lines only performs two actions, but very important ones. The only marker that is available in the scene is selected at random. Then the model is moved to its coordinates. Thus, each subsequent instances of the fish will be scattered in different places. It may happen that there will be several objects at one point. Initially, the chosen variant of the cubic collider often led to the “gluing” of intersecting models. So I had to choose a spherical collider. This solved my problem.

So, in the line with Math.random, a random number from 1 to 4 is generated and stored in the spawn_number variable (in the test example, only 4 spawn objects). The game_config.js file contains an array of spawn objects:
 exports.SPAWN_FISH = ["spawn1","spawn2","spawn3","spawn4"]; 

The function get_object_by_name searches the base scene for an object with the name stored in an array:
 var spawn = m_scenes.get_object_by_name (m_game_config.SPAWN_FISH[spawn_number-1],0); 

Now the model is loaded and moved to the location of the dislocation, it remains to make it visible and enable physics. This is where the first surprise awaits.

The engine API has show_object (obj) and enable_simulation (obj) functions. They just activate the object specified in brackets. In this case, the hierarchy is completely ignored. This is due to the nature of character creation and customization (described in detail in the previous article ). For example, the model hierarchy might look like this:



Any of these items can have physics or be rendered. It is clear that the collider does not show anything. Nevertheless, the decision is shifted to the user, what and how to include.

It would be nice to add functions to the API that perform the same thing, only with the entire hierarchy at once. Indicated the root object and got the result.

Since this is not the case, then you have to go through all the objects and activate them in a loop:
 //        ID var objs = m_scenes.get_all_objects("ALL", data_id); for (var i = 0; i < objs.length; i++) { var obj = objs[i]; //   mesh if (m_obj.is_mesh(obj)) m_scenes.show_object(obj); //     if (m_phy.has_physics(obj)) m_phy.enable_simulation(obj); } ... 

Now physics and visualization are included. It remains to do the main thing - to add a little “brains” to the fish.

I decided to do the following. Make a template for storing local character data and create a working instance based on it. Such a workpiece contains references to objects in the character hierarchy, its logical status (state), and even some temporary variables. The data_id is the number of the loaded scene-model.
 // function template_fish (data_id) { this.root = m_scenes.get_object_by_name ("fish", data_id); this.body = m_scenes.get_object_by_name ("body", data_id); this.state = m_game_config.STATE_FISH_INI; ... } 

The object itself is created as follows:
 var clone_fish = new template_fish(data_id); 

A little earlier, a sensor was created (the function create_elapsed_sensor ) generating cyclic events. It is time to connect a new character to this sensor to create the main logic unit:
 m_ctl.create_sensor_manifold(clone_fish, "FISH", m_ctl.CT_CONTINUOUS, [_elapsed_sensor], null, fish_ai_cb); //      _fishes.push (clone_fish); 

Note that this set ( sensor_manifold ) calls the fish_ai_cb function when an event occurs. It is in it that the main part of logic is concentrated. Switching actions are performed in accordance with the local variable of the state object:
 function fish_ai_cb (clone_fish) { case m_game_config.STATE_FISH_INI: break; case m_game_config.STATE_FISH_MOVE: break; case m_game_config.STATE_FISH_ROTATE: break; ... default: break; } 

For convenience, all possible values ​​of state are in the configuration file:
 //State fish exports.STATE_FISH_INI = 0; exports.STATE_FISH_MOVE = 1; exports.STATE_FISH_ROTATE = 5; exports.STATE_FISH_WAIT = 3; ... 

Now consider the features of the implementation of the movement and the search path. The movable object-fish is a spherical collider. All other objects in the scene also have physical colliders. This allows you to use a raycast to scan the space in front of the fish's nose. If the beam is reflected from any object, then a logical decision is made.

The beam is generated only when the fish moves forward. In all other cases, this is not necessary.

So, there is the following code enclosed in case STATE_FISH_MOVE :
 … //   m_phy.set_character_move_dir(clone_fish.root,1, 0); … // ray test var to = new Float32Array(3); var trans = new Float32Array(3); m_trans.get_translation(clone_fish.root, trans); to = [0,0,1]; clone_fish.ray_id = m_phy.append_ray_test(clone_fish.root, trans, to, "ANY", ray_test_cb, true); ... 

In accordance with the purpose of the logical block, the object is moved here. In general, Blend4Web offers two modules for this purpose: physics (using physics) and transform (the usual change of vectors). Since I use colliders, as well as the raycast function, all the movement and rotation of objects need to be done only with the help of physics.

The set_character_move_dir (clone_fish.root, 1, 0) function just moves an object in the specified direction (forward). And this happens regardless of the main stream. That is, having called this function once, you will receive an infinite movement (speed and other physics parameters are set in Blender. See the lesson “ Preparing a Character for Blend4Web ”).

Further along the code is the raycast generation. The append_ray_test function requires a number of parameters:


In accordance with the specified parameters, the following occurs. The function “sends” the beam in the specified direction ( to ). When any object is detected, ray_test_cb is called. After that, raycast stops working. Note that append_ray_test also works in asynchronous mode.

So, while the state = “move” mode is set, the fish moves forward and at the same time “probes” the space in front of the nose. If an obstacle is detected, the movement stops, the status changes to “rotate” and, accordingly, a reversal is performed.

Practically this is solved with the use of crutches. The problem was that the ray_test_cb callback does not return a reference to the object that called the raycast. And after all, it is for him to change the state mode. It is good that when creating a raycast, the identification number of the “ray” is created, which is then transmitted to the callback.

I had to create a special variable ray_id for the object in order to store the id of the append_ray_test function. By simply sorting through the id_ray of all the fish, the culprit is found and the state is set:
 function ray_test_cb (id, hit_fract, obj_hit) { for (var i = 0; i < _fishes.length; i++) { if (_fishes[i].ray_id ==id) _fishes[i].state = m_game_config.STATE_FISH_ROTATE; } } 

The spread itself is performed as follows:
 ... var elapsed = m_ctl.get_sensor_value(clone_fish, "FISH", 0); m_phy.character_rotation_inc(clone_fish.root, elapsed * -4, 0); ... 

Function character_rotation_inc (obj, h_angle, v_angle)
rotates the object to the specified angle, where h_angle is the horizontal angle value, v_angle is the verticals. And here, attention! For the same rotational speed, regardless of the power of the computer, you must use the return time from the elapsed_sensor sensor (first line). Multiply the value obtained by the desired angle of rotation.

The result of the work


I am developing this game, first of all, to explore the possibilities of Blend4Web. By trial and error, the code is created, as well as the b4w’s incomprehensible or weak points.

The most important thing that I lacked was debug-functions. For example, the ability to create a line between two points. This could be used to test raycast ray. Or visualization of colliders. And it is the physical body, not the primitive, that is created in Blender.

It would be nice to create vector direction constants, such as Forward, Back, Left, Right. The fact is that the coordinate axes of Blender and WebGL do not match. Faster writing Vec3.Forward, rather than experimenting and looking for a suitable combination [0,0,1].

Random number generator. Yes, you can write Math.floor (Math.random () * (5 - 1)) + 1 , but it would be easier to take a ready-made function.

Nevertheless, the code was written, the fish are swimming and even a little “wiggling their brains”. To be continued…

Update
For some reason, the application is not loaded in firefox, although there are no problems with the browser on the local server, but the download scripts will definitely be able to download :)
Test scene + scripts .

Source: https://habr.com/ru/post/269779/


All Articles