Teenage Mutant
Ninja Turtles
Danger of the Ooze October 2014 - WayForward - for
XBox 360, PlayStation 3, and Nintendo 3DS
Developed by
WayForward and published by Activision,
players take control of their favorite
turtle in a Metroid-vania style adventure.
I did a lot of work on this game that I am
very proud of! The biggest and ugliest of my
tasks was building a coroutine-scheduler
into C++, which then allowed us to write
entity behaviors like simple linear scripts
rather than complicated state machines.
Making this run as efficiently on the
Nintendo 3DS as it did on the PS4 involved
me learning ARM-9 assembly language to
manually manipulating stack pointers for
saving and restoring stack data as we YIELD
and resume yielded methods.
Another big task I had was writing a vision
system for enemy soldiers to detect the
player during stealth sections. The sight
cones even wrap around world geometry! (Pics
and details below)
I also programmed enemy and boss AI,
developed various UI systems, and wrote a
Super Metroid-style world map system for
tracking player progress. I even got to
write all of the logic for the final boss
fight against the Shredder.
You can see my boss scene below. SPOILER
WARNING!
These pics show off the enemy vision cone system.
The red rectangle is the culling field. Vision
cone checks are only performed by this enemy
entity (Black Foot Soldier, left) while the player
character (Ninja Turtle, right) is within this red
bounding rectangle. The rays making up the top
half of the vision code intersected with the
player, so additional checks (The blue lines) are
made to see if intersections with static geometry
are detected between the player and enemy. Each
ray of the vision cone actually intersects with
static geometry before intersecting with the
player, so the enemy’s vision is blocked and the
Foot Soldier is unable to see the player.
This image shows how the sight cones wrap around
world geometry. You can see in this shot that the
Foot Soldier vision cones are blocked by walls in
the environment and reshape themselves based on
this. These vision cones are not visible in-game.
I also wrote the map system in the game. We used
Super Metroid as the inspiration for how to
construct the map using individual tiles. Each
room type contains 0-4 walls, all of which we can
represent using 6 unique icons and rotations. Each
room in the game contains a single map reference
object with its map coordinates. The player's map
position is calculated from the room's reference
point.
Here are a few examples of our C++ coroutine
methods. These are all attack patterns I wrote for
the Shredder boss fight. This first block creates
the blue projectiles that rain down from the sky
at approx. 2:10 in the above video.
////////////////////////////////////////////////////////////////////////
/// Create blue energy projectiles to rain down on the playing field.
//////////////////////////////////////////////////////////////////////// COROUTINE_CLASSMETHOD_DEF( MutagenAttackBlueComponent, CoroutineCreateDroplets )
MutagenAttackBlueComponentSP spThis = pCoroutine->m_spOwningComponent;
BLACK_ASSERT_MSG( IS_SP_VALID(spThis), "Owning component is invalid." );
GameObjectManager* pGameObjectManager =
spThis->GetOwningGameObject()->GetGameObjectManager();
vec2 vBounds_Left = vec2(spThis->m_vSceneTarget_ProjectileSpawnBoundary_Left);
vec2 vBounds_Right = vec2(spThis->m_vSceneTarget_ProjectileSpawnBoundary_Right);
float fTimeBetweenDropletsSeconds = spThis->m_fTimeBetweenDropletsSeconds;
float fBaseHeight = vBounds_Left.y;
float fZ = 0;
float fHeightInc = 0.0f;
vec2 vProjectileVelocity = vec2( 0.0f, -spThis->m_fProjectileSpeed );
u32 uiNumDroplets = spThis->m_uiNumDroplets;
char pcProjectileName[64];
strcpy( pcProjectileName, spThis->m_pcProjectileGameObjectName );
for (u32 uiDroplet = 0; uiDroplet < uiNumDroplets; ++uiDroplet)
{
// Wait a pre-set time delay before firing the droplet.
// Skip the delay if this is the first drop.
float fCurrentTimeSeconds = pGameObjectManager->GetTotalElapsedWorldTimeSeconds();
float fStartTimeSeconds = fCurrentTimeSeconds;
float fElapsedTimeSeconds = 0.0f;
while ( (fElapsedTimeSeconds < fTimeBetweenDropletsSeconds) && (uiDroplet != 0))
{
YIELD();
fCurrentTimeSeconds = pGameObjectManager->GetTotalElapsedWorldTimeSeconds();
fElapsedTimeSeconds = fCurrentTimeSeconds - fStartTimeSeconds;
}
// Create droplet game object.
GameObjectSP spProjectile = pGameObjectManager->GetGameWorldRoot()->
CreateChildGameObjectFromTemplate( pcProjectileName, true );
// Set droplet's velocity.
PhysicsBody2dComponentSP spProjectilePhysics =
spProjectile->GetPhysicsBody2dComponent();
spProjectilePhysics->SetPersistentMoveVector( vProjectileVelocity );
// Set droplet's position.
float fLerpT = Black::FRand( 0.0f, 1.0f );
float fPosX = LERP( vBounds_Left.x, vBounds_Right.x, fLerpT );
vec3 vPos = vec3(fPosX, fBaseHeight + fHeightInc, fZ);
spProjectile->GetTransformComponent()->SetGlobalPosition(vPos);
}
COROUTINE_RETURN();
}
This second block is the logic for Shredder's red
energy laserbeam attack. You can see this at
approx. 2:47 in the above video.
////////////////////////////////////////////////////////////////////////
/// Create Shredder's red laserbeam attack.
////////////////////////////////////////////////////////////////////////
COROUTINE_CLASSMETHOD_DEF( MutagenAttackRedComponent, CoroutineFireRedMutagen )
MutagenAttackRedComponentSP spThis = pCoroutine->m_spOwningComponent;
BLACK_ASSERT_MSG( IS_SP_VALID(spThis), "Owning component is invalid." );
PhysicsBody2dComponentSP spPhysicsBody2dComponent =
spThis->GetOwningGameObject()->GetPhysicsBody2dComponent();
bool bIsFacingRight = spThis->GetOwningGameObject()-> GetSidescrollerOrientationComponent()->IsFacingRight();
// Play the charge animation.
spThis->EnableChargeVFX( true );
spThis->m_bAnimationSequenceIsDone = false;
spThis->GetOwningGameObject()->Call(
GOF_REQUEST_PLAY_BOSS_ANIMATION,
BOSS_ANIMATION_MUTAGEN_CANNON_ATTACK_CHARGE
);
while (!spThis->m_bAnimationSequenceIsDone)
{
YIELD();
}
// Determine which attack type we're playing.
BossAnimationType eRedMutagenType = BOSS_ANIMATION_MUTAGEN_CANNON_ATTACK_RED_HIGH_TO_LOW;
switch (spThis->m_eMutagenAttackType)
{
case MutagenAttackTypes_Red_Low_To_High:
{
eRedMutagenType = BOSS_ANIMATION_MUTAGEN_CANNON_ATTACK_RED_LOW_TO_HIGH;
}
break;
case MutagenAttackTypes_Red_High_To_Low:
default:
{
eRedMutagenType = BOSS_ANIMATION_MUTAGEN_CANNON_ATTACK_RED_HIGH_TO_LOW;
}
break;
}
// Setup attack type.
spThis->GetOwningGameObject()->Call(
GOF_SHREDDER_SET_DAMAGE_TYPE,
ShredderAIComponent::keShredderAttack_MutagenRed
);
// Play the animation.
spThis->m_bAnimationSequenceIsDone = false;
spThis->GetOwningGameObject()->Call(
GOF_REQUEST_PLAY_BOSS_ANIMATION,
eRedMutagenType
);
spThis->ResetLaser();
while (!spThis->m_bAnimationSequenceIsDone)
{
// Track the gun angle. Rotate the laser VFX to match.
spThis->UpdateGunAngle();
spThis->FireLaser();
YIELD();
}
spThis->EnableChargeVFX( false );
// Setup attack type.
spThis->GetOwningGameObject()->Call(
GOF_SHREDDER_SET_DAMAGE_TYPE,
ShredderAIComponent::keShredderAttack_Touch
);
spThis->GetOwningGameObject()-> Call( GOF_ON_CURRENT_ENEMY_BEHAVIOR_COMPLETED );
COROUTINE_RETURN();
}