Scotty Hoag
Programmer Portfolio
 
scotty.hoag@gmail.com  (209)-298-2622
NES Mug


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!
   
logo

Box Art (Nintendo 3DS)





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(); }