Blog

Making a Game Part 11. Artificial Intelligence

2015-04-20

What is Pyatigor's Tale from the gameplay point of view? It's an arcade game in which the player tries to avoid various pitfalls that occur around Pyatigor, and at the same time he fights enemies which want to vanquish him by any means. Opponents must be assigned some behavior in order for this process to be interesting enough, and for the player to be required to make considerable efforts to defeat them. Let's take a look at how we managed to endow the golems with their own intelligence.

Golem's Life Cycle

Before we start programming, we need to decide what kind of actions are required from our monsters. Thus, let's consider the golem's life cycle.

Golem's life cycle.

  1. First, the golem is born. His intelligence doesn't work until he completely exits the lava.
  2. Having settled on the island, the golem starts to patrol. We want him to continuously cruise from one point on the island to another without falling into lava. If there is a character on the island, the golem immediately switches to him and tries to decrease the distance to inflict some damage. If there is no character on the island, the last thing that can distract the golem is the magic stones. If the obelisk beside the golem is partially or completely filled with stones, the golem will approach it and will try to destroy the gems in order to deter the character from capturing the island.
  3. If there is a character or an obelisk with gems in the golem's scope, he will make an attack.
  4. The only way the golem can finish his life-cycle is to be slayed by the player.

After this, the golem is marked as non-active and the cycle is repeated.

Let's start implementing these points!

Golem's Components

In order to better understand how to reconstruct this behavior, let's take a look at the components the golem consists of.

The main components of the golem.

  1. An armature for the skeletal animation
  2. A body for rendering
  3. A physical object for calculations

All these parts are children of the Empty object, which they are bound to on the scene because it has the Relative Group Coords flag turned on. Therefore, if this Empty is being moved, all the required objects will follow it at the same time. So, we'll be changing only its position with our API.

Initialization

All the work with golems is moved into a separate golems.js module. Each golem's data is packed into a special wrapper-object and is saved into the global _golems_wrappers array for convenience:

function init_golem_wrapper(body, rig, empty) {
    return {
        body: body, // Physical object for the golem's body
        rig: rig, // Armature object for animation
        empty: empty, // Empty object which the golem's group is bound to
        ...
        state: m_conf.GS_NONE,
        ...
    }
}

We won't consider all of this object's properties. It is important to notice that the wrapper has links to the real objects relative to the particular golem in the first three fields.

These objects are: golem's physical object body, an armature rig and an empty object which all other objects are bound to. So, having this wrapper we can perform any actions related to the physics, animation or particular golem's movement.

One more important property - the golem's state. It can have four values: GS_GETTING_OUT - golem is getting out of the lava, GS_WALKING - golem is patrolling, GS_ATTACKING - golem is in the attack process, GS_NONE - golem is inactive.

Behavior Basics

First, let's register a sensor manifold for every golem:

m_ctl.create_sensor_manifold(golem_wrapper, "GOLEM", m_ctl.CT_CONTINUOUS,
                             [elapsed_sensor], null, golem_ai_cb);

golem_ai_cb() callback will be called every frame. Let's take a look at it:

function golem_ai_cb(golem_wrapper, id) {
    if (golem_wrapper.hp <= 0) {
        kill(golem_wrapper);
        return;
    }
    switch (golem_wrapper.state) {
    case m_conf.GS_ATTACKING:
        process_attack(golem_wrapper);
        break;
    case m_conf.GS_WALKING:
        var elapsed = m_ctl.get_sensor_value(golem_wrapper, id, 0);
        move(golem_wrapper, elapsed);
        break;
    default:
        break;
    }
}

If the golem's HP is dropped under zero, his death handler kill() is called. In this function, all the golem wrapper's parameters are being reset and special animation and sound are played. Its code is rather trivial, thus, we won't consider it. The second block is a key part of the golem's intelligence. His movement or attack is processed here depending on the golem_wrapper.state value. If the golem is getting out of the lava or if he is not active, there will be nothing done in this function.

Birth

Let's create a sensor manifold responsible for the golem's resurrection:

m_ctl.create_sensor_manifold(null, "GOLEMS_SPAWN", m_ctl.CT_CONTINUOUS,
                             [elapsed_sensor], null, golems_spawn_cb);

Its handler will have the following form:

function golems_spawn_cb(obj, id) {

    var golem_wrapper = get_first_free_golem();
    if (!golem_wrapper) // no available golems
        return;

    var elapsed = m_ctl.get_sensor_value(obj, id, 0);
    _golems_spawn_timer -= elapsed;

    if (_golems_spawn_timer <= 0) {
        _golems_spawn_timer = m_conf.GOLEMS_SPAWN_INTERVAL;

        var island_id = get_random_available_island();
        if (island_id == null) // no available islands
            return;

        spawn(golem_wrapper, island_id, spawn_points, spawn_quats);
    }
}

Only three golems can be present on the island at a time. In order for another golem to rise out of the lava, the following requirements have to be met:

  • The island must not be captured by the player
  • There must be no other golem on the island
  • There has to be at least one available golem

WIth the help of the get_first_free_golem() function we'll get the wrapper of the available golem if there are any. Next, we need to decrease the golems spawn_timer and, if it goes below zero, we reset it and with the help of the get_random_available_island() function check if there is an appropriate island. When we ensure that all these conditions are true, we will start the golem's birth.

All we know is the island which the golem should be placed on. It is necessary to decide at what exact point we should place him. To do this, let's place special markers with Empty objects on every island. We'll be choosing a random one and placing a new golem at its position. This information is calculated only once during initialization and is then put into the appropriate spawn_points and spawn_quats arrays, which are forwarded to the spawn() function:

function spawn(golem_wrapper, island_id, spawn_points, spawn_quats) {
    var golem_empty = golem_wrapper.empty;

    var num_spawns = spawn_points.length / m_conf.NUM_ISLANDS;
    var spawn_id = Math.floor(Math.random() * num_spawns);
    var spawn_pt_id = num_spawns * island_id + spawn_id;
    var spawn_point = spawn_points[spawn_pt_id];
    var spawn_quat = spawn_quats[spawn_pt_id];

    m_trans.set_translation_v(golem_empty, spawn_point);
    m_trans.set_rotation_v(golem_empty, spawn_quat);

    set_state(golem_wrapper, m_conf.GS_GETTING_OUT)

    m_sfx.play_def(golem_wrapper.getout_speaker);

    golem_wrapper.island_id = island_id;
    golem_wrapper.dest_pos.set(spawn_point);
}

As we can see, the golem_wrapper copes nicely with its task. With the help of the transform (m_trans) module the golem's Empty object is positioned at a random birth point on the required island. Then the state is switched to GS_GETTING_OUT. In the set_state() function, animation is applied to the golem, and after it is finished, the state is changed to GS_WALKING and the walk animation is played. In the end, a sound of golem getting out is played and the default properties are set for the wrapper. The golem has been born and he is ready to fight!

Patrol

Based on the described requirements of the golem's positioning, we can designate two types of his movement:

  1. Golem just cruises in different directions
  2. Golem is moving toward his target in order to attack
  3. https://www.blend4web.com/en/forums/topic/471/?page=1#post-2093

The first point includes several possible realizations:

  1. One or several points which are constantly "shooting" down with rays and trying to determine the type of the surface the golem will step into are placed in front of him. Based on this info, the golem can make a decision about changing his direction. This option is not appropriate for us because it has a high probability of an error and this method assumes serious calculation loads.
  2. The method is fairly similar to the previous one, but the golem's physical bounding is used instead. When the golem touches a special hidden object placed on the perimeter of the island, he turns away from the point of collision. This method is faster but it also can lead to unpredictable results because we can get only the first collision point with the current API.
  3. And finally, the method we have chosen. This is to use predefined positions and to choose a random one of them. Well, the golem won't actually "decide" where to go and this will require some meta-information (additional coordinates). However, this method is much faster and avoids possible errors related to physics, and stability is what we mostly interested in.

Schematically this behavior can be described with the following animated image:

Golem's movement on the island.

Let's add some more base points for the golem's trajectory with new Empty objects. For optimization reasons, we'll take one point from respawn positions already presented on the island. We'll have the following positions set after these manipulations (marked with red circles):

Base points for the golem's movement.

It is needed to take into account that if the points are too close to each other, some frustrating errors can occur when the golem will try to reach the point which is right at his hand. He will just start rotating on the spot and will never get to the destination.

The function responsible for the golem's movement is the following:

function move(golem_wrapper, elapsed) {

    var char_wrapper = m_char.get_wrapper();
    var island_id = golem_wrapper.island_id;

    if (char_wrapper.island == island_id && char_wrapper.hp > 0) {
        attack_target(golem_wrapper, char_wrapper.phys_body, elapsed);
        golem_wrapper.last_target = m_conf.GT_CHAR;
    } else if (m_obelisks.num_gems(island_id)) {
        var obelisk = get_obelisk_by_island_id(island_id);
        attack_target(golem_wrapper, obelisk, elapsed);
        golem_wrapper.last_target = m_conf.GT_OBELISK;
    } else {
        patrol(golem_wrapper, elapsed);
    }
}

Here we can clearly see the designed strategy. The golem will attack the player if he is on the same island or the obelisk if there are some magic stones in it. In any other case, the patrol() function will be called, and the golem will move between base points on the island. Let's take a look at this function:

function patrol(golem_wrapper, elapsed) {
    set_dest_point(golem_wrapper);
    rotate_to_dest(golem_wrapper, elapsed);
    translate(golem_wrapper, elapsed);
}

At the beginning, the position which the golem will head to is set by the set_dest_point() function. If the golem is close enough to it, the new point will be chosen. Otherwise, the old one will remain. After the new position is written to the golem_wrapper.dest_pos field, the golem turns towards it with the help of the rotate_to_dest() function and then he just moves in his own direction with the translate() function. All these functions have no direct relationship to the golems' intelligence, thus we won't consider them. We just need to know that the Empty object is constantly changing its position as the golem moves.

Attack

The golem noticed the character and moved to attack.

Let's pay attention to the move() function again and let's see how the golem attacks his targets. These actions are described in the attack_target() function which is common for both player and obelisk attacks. They differ only in the last_target field values: GT_CHAR for the character and GT_OBELISK for the obelisk.

function attack_target(golem_wrapper, target, elapsed) {

    var golem_empty = golem_wrapper.empty;
    var trans       = _vec3_tmp;
    var targ_trans  = _vec3_tmp_2;

    m_trans.get_translation(golem_empty, trans);
    m_trans.get_translation(target, targ_trans);
    targ_trans[1] = trans[1];
    golem_wrapper.dest_pos.set(targ_trans);

    var dist_to_targ = m_vec3.distance(trans, targ_trans);
    var angle_to_targ = rotate_to_dest(golem_wrapper, elapsed);

    if (dist_to_targ >= m_conf.GOLEM_ATTACK_DIST)
        translate(golem_wrapper, elapsed);
    else if (angle_to_targ < 0.05 * Math.PI)
        perform_attack(golem_wrapper);
}

The transform module and the rotate_to_dest() function help us to find the distance and the angle to the chosen target. Keep in mind that as we equate targ_trans Y component with the trans Y component it can be said that we work only in two dimensions, and the golem is oriented on the plane surface. If the distance is greater than the reach of the golem's attack GOLEM_ATTACK_DIST, he will move toward the target using the already known translate() function. If the distance is small enough and the angle_to_targ is less than the given limit, the golem will make an attack. Let's take a look at the perform_attack() function:

function perform_attack(golem_wrapper) {

    var golem_empty = golem_wrapper.empty;
    var at_pt       = golem_wrapper.attack_point;
    var trans       = _vec3_tmp;
    var cur_dir     = _vec3_tmp_2;

    golem_wrapper.attack_done = false;
    set_state(golem_wrapper, m_conf.GS_ATTACKING)

    m_trans.get_translation(golem_empty, trans);
    m_vec3.scaleAndAdd(trans, cur_dir, m_conf.GOLEM_ATTACK_DIST, at_pt);
    at_pt[1] += 0.3; // raise attack point a bit

    if (m_sfx.is_play(golem_wrapper.walk_speaker))
        m_sfx.stop(golem_wrapper.walk_speaker);

    m_sfx.play_def(golem_wrapper.attack_speaker);
}

Coordinates of the golem's attack point are written to the golem_wrapper.attack_point field. We'll process them later. The golem_wrapper.attack_done field will also be required in the attack handler. set_state() changes the current state and sets the needed animation. Sounds are processed at the end: the walk_speaker sound is turned off if it is playing and the attack_speaker sound is turned on.

The golem attacks the obelisk.

Let's return to the main intelligence handler now. We have seen the following code there:

switch (golem_wrapper.state) {
case m_conf.GS_ATTACKING:
    process_attack(golem_wrapper);
    ...
}

Therefore, when the golem's state changes to attack, the process_attack() function will be called in every frame until his state is changed to something else. Let's look at the attack handler.

function process_attack(golem_wrapper) {
    if (!golem_wrapper.attack_done) {
        var frame = m_anim.get_frame(golem_wrapper.rig);
        if (frame >= m_conf.GOLEM_ATTACK_ANIM_FRAME) {
            if (golem_wrapper.last_target == m_conf.GT_CHAR)
                process_golem_char_attack(golem_wrapper);
            else if (golem_wrapper.last_target == m_conf.GT_OBELISK)
                process_golem_obelisk_attack(golem_wrapper);
            golem_wrapper.attack_done = true;
        }
    }
}

The attack_done flag is used as the main criterion here. Attack animation has a specific length but we want the attack handler to be called only once at some predefined moment. Thus, we check whether the animation has reached the needed GOLEM_ATTACK_ANIM_FRAME and then, based on the type of the target being attacked last_target, either process_golem_char_attack() or process_golem_obelisk_attack() is called. The content of these functions is not really important to us and, perhaps, it is enough code for this tutorial. In the first function, we check if the attack on the character succeeded, depending on his distance to the golem's attack point, and if this condition is met, the character's HP is reduced. In the second function, there isn't even a need to check the distance because the obelisk is not capable of running away.

This is how the golems' behavior is organized. We can diversify it by adding some interesting abilities and processing them as new golem_wrapper.state types, but now we'll stay with what we have as our golems don't have any special skills and can only walk and hit. And this is enough to make the player's life a bit more difficult.

Launch the game!

The source files are part of the free Blend4Web SDK.

Changelog

[2015-04-20] Initial release.

Comments
19 mar. 2016 08:46
good
09 nov. 2016 10:35
The game only loads the top GUI bar and rest is black on Chrome and Firefox iPhone
13 mar. 2021 02:12
Not everyone, unfortunately, is good at completing such an assignment on their own, and at a time when a student is caught up in panic and frustration, companies such as how to write a biography come to his aid, offering to order an Essay from professionals.
27 mar. 2021 16:18
双指数移动平均线(DEMA)
双指数移动平均线(DEMA)减少了传统EMA的滞后时间,使其更加灵敏,更适合短期交易者。 DEMA由Patrick Mulloy开发,并在1994年1月的《股票和商品技术分析》杂志中引入。
叠加使用单平滑EMA和双平滑EMA之间的延迟差来抵消单平滑EMA。此偏移产生的移动平均线保持平滑,但与单平滑或双平滑传统EMA相比,更靠近价格柱。
计算
单平滑和双平滑EMA:
EMA1 =价格EMA
EMA2 = EMA1的EMA
DEMA =(2 x EMA1)-EMA2
从本质上讲,该公式采用了稍微滞后的单平滑EMA1和甚至更滞后的双平滑EMA2之间的滞后差,然后从EMA1中减去该差。此计算产生的平滑线比EMA1或EMA2更接近价格线。
free forex signals
解释
DEMA的解释与传统EMA相似,但响应速度更快。像其他EMA一样,它可以用于确认趋势或发现趋势中的变化。
forex signals
最常用的信号是DEMA分频器。留意DEMA线越过价格柱,或观察短期DEMA越过长期DEMA,以指示趋势变化。例如,超过50天DEMA的20天DEMA将是看涨信号。
这些DEMA交叉(无论是价格还是其他DEMA)通常比相应的传统EMA交叉发生得早得多。在下面的示例中,绿色箭头标记DEMA交叉点,蓝色箭头标记相应的EMA交叉点。在这两种情况下,DEMA交叉都发生在EMA交叉之前。
https://www.freeforex-signals.com/
请记住,由于DEMA的反应比传统EMA更快,因此可能需要调整您的交易策略以用于DEMA。
结论
与传统EMA相比,DEMA通常会出现价格交叉和其他信号。 DEMA的减少滞后和更强的响应能力吸引了短期投资者,但长期投资者可能会发现传统的移动平均线更有用。与所有技术指标一样,交易者应将DEMA与其他指标和分析技术结合使用。
27 mar. 2021 17:07
Help Me Write My Assignment
31 mar. 2021 20:19
考夫曼的自适应移动平均线(KAMA)
由佩里·考夫曼(Perry Kaufman)开发的考夫曼的自适应移动平均线(KAMA)是一种移动平均线,旨在解决市场噪音或波动性。当价格波动相对较小且噪音较低时,KAMA将密切关注价格。当价格波动扩大时,KAMA会进行调整,并从更远的距离跟踪价格。该趋势跟踪指标可用于识别总体趋势,时间拐点和过滤价格走势。
https://www.freeforex-signals.com/
用法和信号
图表分析师可以像其他任何趋势追踪指标(例如移动平均线)一样使用KAMA。图表专家可以寻找价格交叉点,方向变化和过滤后的信号。
首先,KAMA上方或下方的交叉线表示价格的方向变化。如同任何移动平均线一样,简单的分频系统将生成许多信号和许多鞭子。租船者可以通过对分频器应用价格或时间过滤器来减少鞭打。有人可能会要求价格将十字架保持设定的天数,或者要求十字架超过KAMA设定的百分比。
其次,图表师可以使用KAMA的方向来定义证券的总体趋势。这可能需要调整参数以进一步平滑指示器。图表专家可以更改中间参数,该参数是最快的EMA常数,以平滑KAMA并寻找方向变化。只要KAMA在下跌并创出更低的低点,趋势就会下降。只要KAMA在上升并创出更高的高点,趋势就会上升。下面的Kroger示例显示了KAMA(10,5,30)从12月到3月有一个陡峭的上升趋势,从5月到8月有一个不太陡峭的上升趋势。
free forex signals

最后,图表专家可以结合信号和技术。图表专家可以使用较长的KAMA来定义较大的趋势,而可以使用较短的KAMA来交易信号。例如,KAMA(10,5,30)可用作趋势过滤器,在上升时被视为看涨。一旦看涨,当价格超过KAMA(10、2、30)时,图表管理员便可以寻找看涨的交叉。下例显示了MMM,其长期KAMA上升,并且在12月,1月和2月出现交叉看涨。长期KAMA在4月下降,5月,6月和7月出现看跌交叉。
Please register or log in to leave a reply.