Fall Fury: Part 8 - Element Interaction

Element Interaction

During the gameplay multiple entities interact with each other to make the gaming experience what it is. The bear collides with obstacles and collects buttons, monsters shoot shells that can fly off-screen or hit the bear—all this is possible with the help of the basic collision detection techniques that are implemented in FallFury.

Check out the video for this article at http://channel9.msdn.com/Series/FallFury/Part-8-Element-Interaction . For a complete, offline version of this series, you may download a nicely formatted PDF of all the articles.

Buttons

Buttons are bonus-boosters that can be placed by the level designer anywhere on the screen in game mode. These are relatively small entities, which are displaced vertically with each cycle of the Update loop and move in the opposite direction, but with the same velocity, as the main character.

image

Looking at the Update function in the GamePlayScreen class, you will notice this call:

UpdateButtons();

UpdateButtons can be considered the button manager function responsible for removing the collected buttons from the rendering stack, counting them, and checking for a button collision when the bear is in close proximity. The implementation looks like this:

void GamePlayScreen::UpdateButtons()
{
    Windows::Foundation::Rect livingEntityBoundingBox = GameBear->GetBoundingBox();
    for (auto button = m_buttons.begin(); button != m_buttons.end();)
    {
        (*button)->Position.x = (*button)->PixelDiff + LoBoundX;
        (*button)->Position.y -= GameBear->Velocity.y;
        if (Geometry::IsInProximity(GameBear->Position,(*button)->Position, 100))
        {
            Windows::Foundation::Rect obstacleRect = (*button)->GetBoundingBox();
            if (livingEntityBoundingBox.IntersectsWith(obstacleRect))
            {
                AudioManager::AudioEngineInstance.StopSoundEffect(Coin);
                AudioManager::AudioEngineInstance.PlaySoundEffect(Coin);
                m_particleSystem.ActivateSet("Sparkle", (*button)->Position,float2(RandFloat(-6.0f,6.0f),RandFloat(-10.0f, -5.0f)));
                StaticDataHelper::ButtonsCollected++;
                button = m_buttons.erase(button);
            }
            else
            ++button;
        }
        else
        {
            ++button;
        }
    }
}

For performance reasons, FallFury supports composite bounding box creation as well as simple box creation. As mentioned earlier in the series, the main character is not composed of a single texture, but rather multiple sprites that are cross-positioned to create a single visual entity. To give you a better idea of what composite vs. simple boxing looks like, take a look at the images below:

imageimage

The image on the left shows how each part of the bear has its own bounding box, and each will be used for collision checking. In the image on the right, the bear has a single bounding box, creating minor potential gaps, but gaining performance.

Going back to UpdateButtons, once the bounding box is obtained, I iterate through the button collection and make sure that each item is located in the proper space:

(*button)->Position.x = (*button)->PixelDiff + LoBoundX;
(*button)->Position.y -= GameBear->Velocity.y;

 

The constant position checks are necessary because FallFury supports dynamic orientation changes. When the user switches from portrait to landscape mode and vice-versa, rendered elements on the screen are not automatically repositioned. Setting the X position is easy as long as there is a fixed button margin (from the left side of the screen: LoBoundX) and adding it to the current LoBoundX value results in a proper X location. There is no need to do the same check on the Y-axis because the level length remains the same regardless of the current screen orientation. The adjustment made relative the Y position is bound to the bear velocity. If the bear moves slower, buttons will also scroll slower.

Given that all buttons are properly positioned, a proximity check is performed on each button passed through the loop. If the bear position is at least 100 pixels away from the current button, the corresponding bounding box is obtained and an intersect check is performed. In simple boxing mode, this is done via Windows::Foundation::Rect::IntersectsWith:

image

If a collision occurs, the appropriate sound effect is played and a particle set is activated to create a visual notification of the action. After the button counter is incremented, the button is removed from the local collection, effectively being removed from the rendering stack.

Power-ups

As the bear flies towards the end of the level, it might encounter bonuses to improve its ability to fight incoming enemies or protect from damage caused by enemy ammo or obstacles. The process behind displaying power-ups on the screen and determining whether there was a collision with the main character is similar to UpdateButtons.

The core function for this task is UpdatePowerups:

void GamePlayScreen::UpdatePowerups(float timeDelta)
{
    if (m_powerups.size() > 0)
    {
        for (auto powerup = m_powerups.begin(); powerup != m_powerups.end(); powerup++)
        {
            (*powerup)->Update(timeDelta);
            (*powerup)->Position.x = (*powerup)->PixelDiff + LoBoundX;
            (*powerup)->Position.y -= GameBear->Velocity.y;
        }
    }
    CheckForCollisionWithPowerups();
}

 

One difference you probably noticed in the snippet above is the fact that the collision check is now done through a separate function—CheckForCollisionWithPowerups:

void GamePlayScreen::CheckForCollisionWithPowerups()
{
    Powerup^ currentPowerup;
    Windows::Foundation::Rect livingEntityBoundingBox = GameBear->GetBoundingBox();
    for (auto powerup = m_powerups.begin(); powerup != m_powerups.end();)
    {
        currentPowerup = (*powerup);
        if (Geometry::IsInProximity(GameBear->Position,currentPowerup->Position, 100))
        {
            Windows::Foundation::Rect obstacleRect = currentPowerup->GetBoundingBox();
            if (livingEntityBoundingBox.IntersectsWith(obstacleRect))
            {
                AudioManager::AudioEngineInstance.PlaySoundEffect(GenericPowerup);
                GameBear->PickupPowerup(currentPowerup, &m_particleSystem);
                powerup = m_powerups.erase(powerup);
            }
            else
            ++powerup;
        }
        else
        {
            ++powerup;
        }
    }
}

If an intersection is detected between the boxed bear and the power-up texture, the current power-up is passed to the Bear instance and the appropriate type of action is selected:

void Bear::PickupPowerup(Powerup^ powerup, ParticleCore* ParticleSystem)
{
    switch (powerup->Type)
    {
        case PowerupType::PARACHUTE:
        {
            m_previousVelocity = Velocity.y;
            SetParachute(powerup->Lifespan);
            Velocity.y -= powerup->Effect;
            ParticleSystem->ActivateSet("ScalableParachute", Position, 0.0f, false, true);
            break;
        }
        case PowerupType::HEALTH:
        {
            CurrentHealth = MaxHealth;
            ParticleSystem->ActivateSet("ScalableHeart", Position, 0.0f, false, true);
            break;
        }
        case PowerupType::BUBBLE:
        {
            if (IsHelmetEnabled)
            {
                IsHelmetEnabled = false;
                DamageDivider = 1.0f;
            }
            else
            {
                DamageDivider = powerup->Effect;
            }
            IsBubbleEnabled = true;
            m_maxBubbleCounter = powerup->Lifespan;
            ParticleSystem->ActivateSet("ScalableBubble", Position, 0.0f, false, true);
            break;
        }
        // [...]
        case PowerupType::BOOMERANG:
        {
            m_weaponType = powerup->Type;
            m_weaponTexture = m_boomerangTexture;
            CurrentDamage = powerup->Effect;
            m_weaponSize = float2(225.0f, 205.0f) * 0.5f;
            ParticleSystem->ActivateSet("ScalableBoomerang", Position, 0.0f, false, true);
            break;
        }
    }
}

Depending on the power-up, textures are added to the bear model and later passed to the rendering stack (if the power-up type is PARACHUTE), some textures and capabilities are replaced (BOOMERANG), or the bear capabilities are temporarily modified (BUBBLE):

image

If one of the temporary power-ups is enabled n the Update loop, dedicated timers ensure that ability enhancement does not last longer than necessary. As an example, here is the snippet that controls the bubble:

if (IsBubbleEnabled)
{
    m_currentBubbleCounter += timeDelta;
    if (m_currentBubbleCounter > m_maxBubbleCounter)
    {
        IsBubbleEnabled = false;
        DamageDivider = 1.0f;
        m_currentBubbleCounter = 0.0f;
    }
}

 

Ammo Collisions

There are also objects that direct ammo at either the enemy or the bear. After release, each shell follows a linear path towards the target, and while the user directs the shells that originate from the bear, those released by enemy entities automatically target the bear. In this case, the ammo needs to collide with an object to either damage or kill it. When a shell is released, it is added to the general ammo collection that is later updated internally:

void GamePlayScreen::UpdateAmmo(float timeDelta)
{
    for (auto shell = m_ammoCollection.begin(); shell != m_ammoCollection.end(); shell++)
    {
        (*shell)->Update(timeDelta, &m_particleSystem);
    }
}

 

At this point, the rendering system does not differentiate between friendly and enemy ammo—all it knows is that each item in the collection must have a new position when a new frame is rendered. The CheckForCollisionsWithAmmo function checks for ammo collisions:

void GamePlayScreen::CheckForCollisionWithAmmo(LivingEntity^ entity)
{
    if (entity != nullptr)
    {
        Windows::Foundation::Rect livingEntityBoundingBox = entity->GetBoundingBox();
        if (entity->IsFriendly)
        {
            for (auto ammo = m_ammoCollection.begin(); ammo != m_ammoCollection.end();)
            {
                if (!(*ammo)->IsFriendly)
                {
                    Windows::Foundation::Rect ammoBoundingBox = (*ammo)->GetBoundingBox();
                    if (livingEntityBoundingBox.IntersectsWith(ammoBoundingBox))
                    {
                        m_particleSystem.ActivateSet("SmallExplosion",entity->Position, true);
                        entity->InflictDamage((*ammo)->HealthDamage);
                        GameBear->RedShade();
                        AudioManager::AudioEngineInstance.PlaySoundEffect(OuchA);
                        AudioManager::AudioEngineInstance.PlaySoundEffect(HardSoftCollision);
                        CheckBearHealth();
                        ammo = m_ammoCollection.erase(ammo);
                    }
                    else
                    {
                        ++ammo;
                    }
                }
                else
                {
                    ++ammo;
                }
            }
        }
        else
        {
            for (auto ammo = m_ammoCollection.begin(); ammo != m_ammoCollection.end();)
            {
                if ((*ammo)->IsFriendly)
                {
                    Windows::Foundation::Rect ammoBoundingBox = (*ammo)->GetBoundingBox();
                    if (livingEntityBoundingBox.IntersectsWith(ammoBoundingBox))
                    { 
                        Monster^ monster = ((Monster^)entity);
                        if (!monster->IsDead && monster->IsActive)
                        {
                            m_particleSystem.ActivateSet("SmallExplosion", entity->Position, true);
                            entity->InflictDamage((*ammo)->HealthDamage);
                            monster->RedShade();
                            monster->CheckIfAlive();
                            AudioManager::AudioEngineInstance.PlaySoundEffect(SharpSoftCollision);
                        }
                        ammo = m_ammoCollection.erase(ammo);
                    }
                    else
                    {
                        ++ammo;
                    }
                }
                else
                {
                    ++ammo;
                }
            }
        }
    }
}

When the function is called, it is usually run against an entity that is present on the screen, such as the main character. Regardless of whether the enemy or the friendly character fired the shot, the shot cannot inflict damage to its source, and that’s why the function implements the ammo-to-entity crosscheck. If the ammo collides with any shells intersecting the entity bounding box, a collision is counted and health verification is performed to ensure that the character is still alive and that the game should continue. The bear’s health is checked via CheckBarHealth:

void GamePlayScreen::CheckBearHealth()
{
    if (GameBear->CurrentHealth <= 0)
    {
        m_particleSystem.ActivateSet("Buttons",GameBear->Position, true);
        GameBear->Kill();
        StopBackground();
    }
}

That said, not all ammo will collide with entities on the screen. Some of it will go out-of-bounds, and without an explicit cleanup process in place out-of-bounds ammo is constantly re-rendered even though the end user has no way of seeing it. To avoid this, there is a helper function—CheckForOutOfBoundsAmmo:

void GamePlayScreen::CheckForOutOfBoundsAmmo()
{
    for (auto l_Iter = m_ammoCollection.begin(); l_Iter != m_ammoCollection.end(); /* nothing here */ )
    {
        if (!GamePlayScreen::Manager->IsWithinScreenBoundaries((*l_Iter)->Position))
        {
            l_Iter = m_ammoCollection.erase(l_Iter);
        }
        else
        {
            ++l_Iter;
        }
    }
}

If any shell flies outside the screen bounding box, its instance is erased from the collection and the renderer no longer worries about allocating memory for an irrelevant item. To give you an idea of how that happens, here is a snippet that shows how the RenderScreen function handles the current ammo set:

if (!m_ammoCollection.empty())
{
    for (auto shell = m_ammoCollection.begin(); shell != m_ammoCollection.end(); shell++)
    {
        if (!(*shell)->IsFriendly)
        {
            (*shell)->Render();
        }
        else
        {
            GameBear->RenderShell((*shell)->Position, (*shell)->Rotation);
        }
    }
}

 

Conclusion

Element interaction is a core part of the FallFury experience. Separate handlers are implemented for each of them to ensure maximum flexibility when it comes to adding or removing components without breaking major parts of the code-base. Handling is mainly accomplished in the Update loop by iterating through registered entity sets, such as ammo, and verifying whether an action should be taken. Be cautious when implementing this kind of scenario with large entities and data sets—having multiple loops running simultaneously might tax machine performance, especially on low-power configurations such as ARM.

Tags:

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.