Blastrobots
Blastrobots is a shared-screen multiplayer game inspired by the classic arcade style and built using UDK. It features a variety of weapons, a unique weapon combination system and a cooperative revive mechanic. I worked on this project as a programmer. My responsibilities included the game’s artificial intelligence systems, implementing gameplay mechanics and providing scripting support for the level design team. The game was developed by a team of thirteen over four months. The Wasted Talent team was extremely passionate and I enjoyed collaborating as a team and with each discipline independently. |
Developing the AI
Developing the AI
Collaborating with the Game Designer and Level Designers, I implemented the enemy AI for Blastrobots. Our iterative design process allowed us to prototype enemies, find what about them was fun and what needed to be changed, then keep refining that prototype. The result is a wide variety of enemies, each with a unique personality in movement and attack styles.
Enemy Types
- Astronaut
-Fodder enemy, keeps at mid-range and fires periodically - Rushbot
-Flies up to player and explodes - Laser Guardbot
-Darts around screen, pausing to shoot players from long-range - Flamethrower Guardbot
-Agressive short-range enemy, periodically overheats and explodes when killed - Rocket Guardbot
-Exclusively long-range enemy, fires volleys of rockets - Spread Guardbot
-Darts in and out of combat, turns on shield then fires extended volley of shots - Mobile Spanwer
-Meanders around combat area, extremely durable and spawns other enemies - Turette(Boss)
-Final boss for campaign, progresses through three phases and utilizes a variety of attacks
View Code
class BlastroAI extends UTBot abstract; const MoveAngleGraduations = 16; const OVERMOVE_RATIO = 2.6; const STRAFE_ATTEMPTS = 4; //========================================================================= //========================================================================= enum AIAction { AI_WAIT, AI_APPROACH, AI_FLEE, AI_WANDER }; var EnemyPawn ownPawn; var BlastroPawn curTarget; var AIAction curAction; var Vector curDestination; var Vector destDisp; var float destDist; var bool recentlyBumped; var Vector bumpVec; var float moveStepDist; var Vector lastMoveVec; // For movement sound // var SoundCue MovementSound; var AudioComponent MovementSoundLoop; var bool bShouldLoopMovementSound; // Subclass preferences var float MinPreferredDistance; var float MaxPreferredDistance; var float RangeTolerance; var float FireRate; var float MinFireRange; var float MaxFireRange; var bool bKeepPlayerInSight; var float DecisionRate; var float curDecisionRate; var bool bFaceTarget; var BlastroWeapon aiWeapon; var class< BlastroWeapon > aiWeaponClass; var float AIDifficulty; //========================================================================= // Difficulty settings var float EasyFireRate; var float HardFireRate; var float InsaneFireRate; var float EasyHealthModifier; var float HardHealthModifier; var float InsaneHealthModifier; var float EasySpeedModifier; var float HardSpeedModifier; var float InsaneSpeedModifier; //========================================================================= function HearNoise( float Loudness, Actor NoiseMaker, optional name NoiseType ) { } //========================================================================= function AdjustToCurrentDifficulty() { local EnemyPawn myPawn; myPawn = EnemyPawn( Pawn ); myPawn.currentDifficultyHealthScale = DifficultyLerp( EasyHealthModifier, 1.0, HardHealthModifier, InsaneHealthModifier ); myPawn.MaxDesiredSpeed *= DifficultyLerp( EasySpeedModifier, 1.0, HardSpeedModifier, InsaneSpeedModifier ); FireRate = DifficultyLerp( EasyFireRate, default.FireRate, HardFireRate, InsaneFireRate ); } //========================================================================= function float DifficultyLerp( float easy, float normal, float hard, float insane ) { local float alpha; alpha = AIDifficulty; if ( alpha < 0.0 ) alpha = 0.0; if ( alpha > 5.0 ) alpha = 5.0; if ( AIDifficulty < 1.0 ) return Lerp( easy, normal, AIDifficulty ); else if ( AIDifficulty < 2.0 ) return Lerp( normal, hard, AIDifficulty - 1.0 ); else return Lerp( hard, insane, ( AIDifficulty - 2.0 ) / 3.0 ); } //========================================================================= //========================================================================= function PostBeginPlay() { Super.PostBeginPlay(); AIDifficulty = BlastroGame( WorldInfo.Game ).globalDifficulty; } //========================================================================= //========================================================================= function Possess( Pawn aPawn, bool bVehicleTransition ) { Super.Possess( aPawn, bVehicleTransition ); AdjustToCurrentDifficulty(); EnemyPawn( Pawn ).ReassessHealth(); aiWeapon = GivePawnBlastroWeapon( aiWeaponClass ); } //========================================================================= //========================================================================= function BlastroWeapon GivePawnBlastroWeapon( class< BlastroWeapon > weaponType ) { local BlastroWeapon newWeapon; local UTPawn myPawn; if ( weaponType == none ) return none; myPawn = UTPawn( Pawn ); if ( myPawn == none ) return none; // Spawn the blastro weapon newWeapon = Spawn( weaponType, self, , , , , ); newWeapon.GiveTo( Pawn ); Pawn.InvManager.SetCurrentWeapon( newWeapon ); myPawn.CurrentWeaponAttachment.AttachTo( myPawn ); // hide the weapon attachment mesh, but leave effects visible myPawn.CurrentWeaponAttachment.SetHidden( false ); myPawn.CurrentWeaponAttachment.Mesh.SetRotation( rot(0, -16384, 0) ); // all are weapon attachments are sideways myPawn.CurrentWeaponAttachment.AttachComponent( myPawn.CurrentWeaponAttachment.Mesh ); myPawn.CurrentWeaponAttachment.Mesh.SetHidden( true ); newWeapon.SetOwner( Pawn ); newWeapon.Instigator = Pawn; return newWeapon; } //========================================================================= //========================================================================= function SetMoveMultiplier( float multiplier ) { if ( Pawn != none ) { Pawn.MaxDesiredSpeed = multiplier; moveStepDist = ownPawn.GroundSpeed * DecisionRate * OVERMOVE_RATIO * ownPawn.MaxDesiredSpeed; } } //========================================================================= //========================================================================= function Initialize(float InSkill, const out CharacterInfo BotInfo) { Super.Initialize( InSkill, BotInfo ); RotationRate.Yaw = 1.0; RotationRate.Pitch = 1.0; RotationRate.Roll = 1.0; AcquisitionYawRate = 1.0; } //========================================================================= //========================================================================= event bool NotifyBump(Actor Other, Vector HitNormal) { recentlyBumped = true; bumpVec = HitNormal; StopLatentExecution(); curDecisionRate = 0.15; Disable( 'NotifyBump' ); Settimer( 0.20, false, 'EnableBumps' ); SetTimer( 0.01, false, 'AILogicTick' ); return false; } //========================================================================= //========================================================================= function EnableBumps() { enable('NotifyBump'); } protected event ExecuteWhatToDoNext() { GotoState( 'BlastroAIMain' ); } //========================================================================= //========================================================================= event WhatToDoNext() { if (Pawn == None) return; RetaskTime = 0.0; DecisionComponent.bTriggered = true; } //========================================================================= //========================================================================= function bool DoPreventDeath() { return false; } //========================================================================= //========================================================================= function bool Invincible() { return false; } //========================================================================= //========================================================================= event AIFireTick() { local Vector disp; local float distSq; if ( ownPawn == none ) return; // Do we see our target? if ( curTarget != none && FastTrace( curTarget.Location, Pawn.Location ) ) { disp = curTarget.Location - Pawn.Location; distSq = disp dot disp; // Is our target close enough? if ( distSq >= MinFireRange * MinFireRange && distSq <= MaxFireRange * MaxFireRange ) AIFire(); } if ( FireRate != 0 ) SetTimer( FireRate + Rand( FireRate * 100 ) / 50, false, 'AIFireTick' ); } //========================================================================= //========================================================================= function AIFire() { // Override in subclasses } //========================================================================= //========================================================================= function ReassessTarget() { local BlastroPawn curPlayerPawn; local BlastroPawn closestValidPawn; local Vector disp; local float closestDist; closestValidPawn = none; foreach AllActors( class'BlastroPawn', curPlayerPawn ) { if ( !curPlayerPawn.IsAliveAndWell() || curPlayerPawn.bIsOverheating ) continue; if ( closestValidPawn == none ) { closestValidPawn = curPlayerPawn; disp = Pawn.Location - curPlayerPawn.Location; closestDist = disp dot disp; } else { disp = Pawn.Location - curPlayerPawn.Location; if ( disp dot disp < closestDist ) { closestValidPawn = curPlayerPawn; closestDist = disp dot disp; } } } curTarget = closestValidPawn; } //========================================================================= //========================================================================= function SetAction( AIAction act, Vector dest ) { curAction = act; curDestination = dest; if ( curAction == AI_WAIT ) { if ( curTarget == none ) Pawn.SetDesiredRotation( Pawn.Rotation, true, false, 0.0, false ); // If waiting, don't play movement sound loop // if ( MovementSoundLoop != none ) MovementSoundLoop.Stop(); } else { Pawn.LockDesiredRotation( false, true ); } StopLatentExecution(); } //========================================================================= //========================================================================= function bool MoveTowards( Actor A ) { if ( NavigationHandle.ActorReachable( A ) ) { // Target can be reached by directly walking curDestination = A.Location; } else { // Target _cannot_ be reached by directly walking NavigationHandle.SetFinalDestination( A.Location ); // Clear cache and constraints (ignore recycling for the moment) NavigationHandle.PathConstraintList = none; NavigationHandle.PathGoalList = none; // Create constraints class'NavMeshPath_Toward'.static.TowardGoal( NavigationHandle, A); class'NavMeshGoal_At'.static.AtActor( NavigationHandle, A ); if ( !NavigationHandle.FindPath() ) return false; NavigationHandle.GetNextMoveLocation( curDestination, Pawn.GetCollisionRadius() ); } SetAction( AI_APPROACH, curDestination ); return true; } //========================================================================= //========================================================================= function bool FleeFrom( Actor A ) { local Vector idealVec; local Vector testDest; local Rotator curAngle; local int flip; for ( idealVec = Normal( Pawn.Location - A.Location ) * moveStepDist; curAngle.Yaw <= 65536 * 5 / 16; curAngle.Yaw += 65536 / MoveAngleGraduations ) { for ( flip = 0; flip < 2; ++flip ) { testDest = Pawn.Location + TransformVectorByRotation( curAngle, idealVec, ( flip == 1 ) ); // Can we get there in the first place? if ( NavigationHandle.PointReachable( testDest ) ) { // This is our final choice if we we can see the player from that // spot *or* we don't care about keeping the player in sight if ( !bKeepPlayerInSight || FastTrace( testDest, curTarget.Location ) ) { SetAction( AI_FLEE, testDest ); return true; } } } } // Couldn't find any viable flee point return false; } //========================================================================= //========================================================================= function bool IsPositionSuitable( Vector testPos ) { local Vector testDisp; local float testDistSq; local Vector curDisp; local float curDistSq; local Vector moveVec; // Test for range problems (irrelevant if no target) if ( curTarget != none ) { testDisp = testPos - curTarget.Location; testDistSq = testDisp dot testDisp; curDisp = curTarget.Location - Pawn.Location; curDistSq = curDisp dot curDisp; moveVec = testPos - Pawn.Location; if ( testDistSq > MaxPreferredDistance * MaxPreferredDistance ) { // If we were close enough before, this position is bad if ( curDistSq <= MaxPreferredDistance * MaxPreferredDistance ) return false; // If we are too far away, this would need to move us closer if ( curDisp dot moveVec <= 0 ) return false; } else if ( testDistSq < MinPreferredDistance * MinPreferredDistance ) { // If we were too close before, this position is abd if ( curDistSq >= MinPreferredDistance * MinPreferredDistance ) return false; // If we are too close, this would need to move us away if ( curDisp dot moveVec >= 0 ) return false; } } // Check for sight if ( curTarget != none && bKeepPlayerInSight && !FastTrace( testPos, curTarget.Location ) ) return false; return true; } //========================================================================= //========================================================================= function bool KeepMoving( int error = 0 ) { local Vector testDest; local Rotator errorAngle; errorAngle.Yaw = Rand( error ) - error / 2; if ( lastMoveVec dot lastMoveVec == 0.0 ) return false; testDest = Pawn.Location + TransformVectorByRotation( errorAngle, lastMoveVec * moveStepDist ); if ( NavigationHandle.PointReachable( testDest ) && IsPositionSuitable( testDest ) ) { SetAction( AI_WANDER, testDest ); return true; } return false; } //========================================================================= //========================================================================= function bool MoveRandomly() { local Vector basisVec; local int graduation[ MoveAngleGraduations ]; local int remainingAngles; local int i; local int curGraduation; local Vector testDest; local Vector bestDest; local bool foundDest; local Rotator curAngle; basisVec.X = moveStepDist; for ( i = 0; i < MoveAngleGraduations; ++i ) graduation [ i ] = i; remainingAngles = MoveAngleGraduations; foundDest = false; // Pick one of the remaining graduations to try while ( remainingAngles > 0 ) { i = Rand( remainingAngles ); curGraduation = graduation[ i ]; --remainingAngles; graduation[ i ] = graduation[ remainingAngles ]; curAngle.Yaw = curGraduation * ( 65536 / MoveAngleGraduations ); testDest = Pawn.Location + TransformVectorByRotation( curAngle, basisVec ); // Can we get there in the first place? if ( NavigationHandle.PointReachable( testDest ) ) { if ( IsPositionSuitable( testDest ) ) { SetAction( AI_WANDER, testDest ); return true; } else { foundDest = true; bestDest = testDest; } } } if ( foundDest ) { SetAction( AI_WANDER, bestDest ); return true; } return false; } //========================================================================= //========================================================================= function bool Strafe() { local Vector basisVec; local Rotator angle; local Vector testPos; local float temp; local int i; local int j; // Move laterally with respect to the target if ( curTarget == none ) return false; basisVec = Pawn.Location - curTarget.Location; basisVec.Z = 0; basisVec = Normal( basisVec ) * moveStepDist; // Fast rotate 90 degrees temp = basisVec.X; basisVec.X = basisVec.Y; basisVec.Y = -temp; // Prefer to keep rotating about the player in the same direction // Of if we weren't moving pick a preference randomly if ( ( lastMoveVec dot basisVec < 0 ) || ( lastMoveVec dot lastMoveVec == 0 && Rand( 2 ) == 0 ) ) basisVec = -basisVec; for ( i = 0; i < 2; ++i ) { for ( j = 0; j < STRAFE_ATTEMPTS; ++j ) { angle.Yaw = Rand( 63336 / 4 ) - 65536 / 8; testPos = Pawn.Location + TransformVectorByRotation( angle, basisVec ); if ( IsPositionSuitable( testPos ) ) { SetAction( AI_WANDER, testPos ); return true; } } basisVec = -basisVec; } return false; } //========================================================================= //========================================================================= function bool PickTargetRelativeDestination() { local Vector targetDisp; local float targetDistSq; local bool bTargetVisible; local bool bNeedToApproach; local bool bNeedToFlee; if ( recentlyBumped && MoveRandomly() ) return true; bTargetVisible = FastTrace( curTarget.Location, Pawn.Location, ); // Do we want to get closer, farther, or some other direction? targetDisp = curTarget.Location - Pawn.Location; targetDistSq = targetDisp dot targetDisp; bNeedToApproach = bKeepPlayerInSight && !bTargetVisible; if ( curAction == AI_APPROACH ) { if ( targetDistSq > ( MaxPreferredDistance - RangeTolerance ) * ( MaxPreferredDistance - RangeTolerance ) ) bNeedToApproach = true; } else { if ( targetDistSq > ( MaxPreferredDistance + RangeTolerance ) * ( MaxPreferredDistance + RangeTolerance ) ) bNeedToApproach = true; } // Is our target too far away? if ( bNeedToApproach ) { if ( !MoveTowards( curTarget ) ) { //`log( "AI [" @ self @ "] unable to reach target, choosing new target" ); //SetAction( AI_WAIT, curDestination ); return false; } return true; } bNeedToFlee = false; if ( curAction == AI_FLEE ) { if ( targetDistSq < ( MinPreferredDistance + RangeTolerance ) * ( MinPreferredDistance + RangeTolerance ) ) bNeedToFlee = true; } else { if ( targetDistSq < ( MinPreferredDistance - RangeTolerance ) * ( MinPreferredDistance - RangeTolerance ) ) bNeedToFlee = true; } // Is our target too close? if ( bNeedToFlee ) { // Attempt to flee if ( FleeFrom( curTarget ) ) return true; } return false; } //========================================================================= //========================================================================= function bool PickWanderDestination() { if ( curAction == AI_WANDER ) { SetAction( AI_WAIT, curDestination ); return true; } else { if ( MoveRandomly() ) return true; } return false; } //========================================================================= //========================================================================= function AILogicTick() { if ( ownPawn == none ) return; // Move about as far in one step as it takes to make our next decision moveStepDist = ownPawn.GroundSpeed * DecisionRate * OVERMOVE_RATIO * ownPawn.MaxDesiredSpeed; //if ( lastMoveVec dot lastMoveVec == 0.0 ) lastMoveVec.X = 1.0; ReassessTarget(); if ( curTarget != none && PickTargetRelativeDestination() ) { } else if ( PickWanderDestination() ) { } else { SetAction( AI_WAIT, Pawn.Location ); } recentlyBumped = false; destDisp = curDestination - Pawn.Location; destDisp.Z = 0; lastMoveVec = Normal( destDisp ); SetTimer( curDecisionRate, false, 'AILogicTick' ); curDecisionRate = DecisionRate; } state Dead { ignores SeePlayer, EnemyNotVisible, HearNoise, ReceiveWarning, NotifyLanded, NotifyPhysicsVolumeChange, NotifyHeadVolumeChange, NotifyHitWall, NotifyBump, ExecuteWhatToDoNext; function BeginState(Name PreviousStateName) { if ( MovementSoundLoop != none && MovementSoundLoop.IsPlaying() ) MovementSoundLoop.Stop(); if ( Class == class'RushbotAI' ) RushbotAI( self ).SirenSound.Stop(); super.BeginState( PreviousStateName ); } } //========================================================================= //========================================================================= state BlastroAIMain { protected event ExecuteWhatToDoNext() { // Ignore UTBot default behaviors } Begin: Instigator = Pawn; ownPawn = EnemyPawn( Pawn ); curAction = AI_WAIT; curDecisionRate = DecisionRate; if ( FireRate != 0 ) SetTimer( 0.1, false, 'AIFireTick' ); SetTimer( 0.1, false, 'AILogicTick' ); if ( MovementSound != none && bShouldLoopMovementSound ) { MovementSoundLoop = Instigator.CreateAudioComponent( MovementSound, false ); } while ( Pawn != none ) { // Play movement loop sound // if ( bShouldLoopMovementSound && !MovementSoundLoop.IsPlaying() ) MovementSoundLoop.Play(); if ( curAction == AI_WAIT ) { if ( bFaceTarget ) MoveTo( Pawn.Location, curTarget ); else MoveTo( Pawn.Location ); Sleep( 0.1 ); } else if ( bFaceTarget ) MoveTo( curDestination, curTarget ); else MoveTo( curDestination ); } MovementSoundLoop.Stop(); ClearAllTimers(); GotoState( 'Dead' ); } //========================================================================= //========================================================================= defaultproperties { MinPreferredDistance = 500.0 MaxPreferredDistance = 1200.0 RangeTolerance = 0.0 FireRate = 1.0 MinFireRange = 0.0 MaxFireRange = 1200.0 MovementSound = none bShouldLoopMovementSound = false DecisionRate = 0.4 bKeepPlayerInSight = true bFaceTarget = true; // Difficulty settings EasyFireRate = 1.0; HardFireRate = 1.0; InsaneFireRate = 1.0; EasyHealthModifier = 0.90; HardHealthModifier = 1.15; InsaneHealthModifier = 2.5; EasySpeedModifier = 1.0; HardSpeedModifier = 1.0; InsaneSpeedModifier = 1.0; }
class AstronautAI extends BlastroAI; var int FireError; var int FloatError; //========================================================================= //========================================================================= function Possess( Pawn aPawn, bool bVehicleTransition ) { Super.Possess( aPawn, bVehicleTransition ); PlayIdleAnim(); } //========================================================================= //========================================================================= function PlayIdleAnim() { EnemyPawn( Pawn ).FullBodyAnimSlot.PlayCustomAnim('BB_astronaut_01_idle', 1.0, 0.1, 0.1, true, true ); } //========================================================================= //========================================================================= function bool PickTargetRelativeDestination() { Pawn.MaxDesiredSpeed = 1.0; if ( Super.PickTargetRelativeDestination() ) return true; Pawn.MaxDesiredSpeed = 0.2; if ( KeepMoving( FloatError ) ) return true; if ( MoveRandomly() ) return true; SetAction( AI_WAIT, curDestination ); return true; } //========================================================================= //========================================================================= function AIFire() { local Vector dir; local Vector idealDir; local Vector disp; local float dist; local Rotator error; local float speedEstimate; if ( curTarget == none ) return; disp = curTarget.Location - Pawn.Location; dist = Sqrt( disp dot disp ); if ( AIDifficulty < 2.0 ) speedEstimate = 0.002; else speedEstimate = 0.001; idealDir = Normal( curTarget.Location + curTarget.Velocity * dist * speedEstimate - Pawn.Location ); dir = TransformVectorByRotation( error, idealDir ); Pawn.SetRotation( rotator(dir) ); if ( AIDifficulty < 2.0 ) aiWeapon.Fire( 0 ); else aiWeapon.Fire( 1 ); EnemyPawn( Pawn ).FullBodyAnimSlot.PlayCustomAnim('BB_astronaut_01_shoot', 1.0, 0.1, 0.1, false, false ); SetTimer( 0.2333, false, 'PlayIdleAnim' ); } //========================================================================= //========================================================================= DefaultProperties { aiWeaponClass = class'BlastroWeaponLemon' //aiWeaponClass = class'BlastroWeaponLockonRocket' bKeepPlayerInSight = true MaxPreferredDistance = 900.0 MinPreferredDistance = 200.0 DecisionRate = 0.4 FireRate = 1.67 MinFireRange = 0.0 MaxFireRange = 1200.0 // 65536 / 4 = 90 degree cone FireError = 2048 FloatError = 2048 // Difficulty settings EasyFireRate = 2.1; HardFireRate = 1.3; InsaneFireRate = 0.4; }
class SpreadGuardbotAI extends BlastroAI; var float SpreadCooldown; var float SpreadWarmup; var int SpreadNumTrails; var int SpreadTrail; var int SpreadRotation; var float SpreadFireRange; var ParticleSystemComponent shieldEffect; enum SpreadState { SPREAD_COOLDOWN, SPREAD_APPROACH, SPREAD_FIRING }; var SpreadState curSpreadState; var float stateStartTime; var int spreadFireCount; var Rotator curSpreadAngle; var int EasyTrails; var int HardTrails; var int InsaneTrails; var int EasyRotation; var int HardRotation; var int InsaneRotation; var float EasyCooldown; var float HardCooldown; var float InsaneCooldown; //========================================================================= //========================================================================= function AdjustToCurrentDifficulty() { Super.AdjustToCurrentDifficulty(); SpreadNumTrails = DifficultyLerp( EasyTrails, default.SpreadNumTrails, HardTrails, InsaneTrails ); SpreadTrail = SpreadNumTrails; SpreadRotation = DifficultyLerp( EasyRotation, default.SpreadRotation, HardRotation, InsaneRotation ); SpreadCooldown = DifficultyLerp( EasyCooldown, default.SpreadCooldown, HardCooldown, InsaneCooldown ); } //========================================================================= //========================================================================= function Possess( Pawn aPawn, bool bVehicleTransition ) { super.Possess( aPawn, bVehicleTransition ); stateStartTime = WorldInfo.TimeSeconds - SpreadCooldown + Rand( SpreadCooldown * 25 ) * 0.01; } //========================================================================= function SetShieldActive( bool bActive ) { local EnemyPawn myPawn; local int i; if ( ( bActive && shieldEffect != none ) || ( !bActive && shieldEffect == none ) ) return; if ( bActive ) { shieldEffect = WorldInfo.MyEmitterPool.SpawnEmitter( ParticleSystem'BB_Item_Particles.Particles.spreadbot_shield_01', Pawn.Location, Pawn.Rotation, Pawn); if ( shieldEffect != none ) shieldEffect.SetScale( 0.5 ); // Turn on shield sound // PlaySound( SoundCue'BlastroSFX.FlameGun.Flame_Shoot_2_Cue' ); myPawn = EnemyPawn( Pawn ); if ( myPawn != none ) { for ( i = 0; i < myPawn.Resistances.Length; ++i ) myPawn.Resistances[ i ].amt = 1.0; } } else { shieldEffect.DetachFromAny(); shieldEffect.DeactivateSystem(); shieldEffect = none; myPawn = EnemyPawn( Pawn ); if ( myPawn != none ) { for ( i = 0; i < myPawn.Resistances.Length; ++i ) myPawn.Resistances[ i ].amt = 0.0; } } } //========================================================================= function SetSpreadState( SpreadState newState ) { if ( newState == curSpreadState ) return; if ( newState == SPREAD_COOLDOWN ) { SetShieldActive( false ); Pawn.MaxDesiredSpeed = 1.0; stateStartTime = WorldInfo.TimeSeconds; } if ( newState == SPREAD_APPROACH ) { if ( AIDifficulty >= 2.0 ) SetShieldActive( true ); Pawn.MaxDesiredSpeed = 3.0; } if ( newState == SPREAD_FIRING ) { SetShieldActive( true ); Pawn.MaxDesiredSpeed = 1.0; spreadFireCount = -2; curSpreadAngle.Yaw = 0; } curSpreadState = newState; } //========================================================================= //========================================================================= event AIFireTick() { local Vector disp; local float distSq; if ( ownPawn == none ) return; if ( curSpreadState == SPREAD_FIRING ) { if ( ++spreadFireCount > SpreadTrail ) { SetSpreadState( SPREAD_COOLDOWN ); } else if ( spreadFireCount >= 0 ) { AIFire(); curSpreadAngle.Yaw += SpreadRotation; } } if ( curSpreadState == SPREAD_COOLDOWN ) { if ( stateStartTime + SpreadCooldown < WorldInfo.TimeSeconds ) SetSpreadState( SPREAD_APPROACH ); } if ( curSpreadState == SPREAD_APPROACH ) { // Do we see our target? if ( curTarget != none && FastTrace( curTarget.Location, Pawn.Location ) ) { disp = curTarget.Location - Pawn.Location; distSq = disp dot disp; // Is our target close enough? if ( distSq >= MinFireRange * MinFireRange && distSq <= MaxFireRange * MaxFireRange ) SetSpreadState( SPREAD_FIRING ); } } SetTimer( FireRate, false, 'AIFireTick' ); } //========================================================================= //========================================================================= function AIFire() { local int i; local Vector dir; local Vector basis; local Rotator angle; local Rotator tmprot; basis.X = 1.0; tmprot = Pawn.Rotation; for ( i = 0; i < SpreadNumTrails; ++i ) { angle.Yaw = curSpreadAngle.Yaw + i * ( 65536 / SpreadNumTrails ); dir = TransformVectorByRotation( angle, basis ); Pawn.SetRotation( Rotator(dir) ); if ( AIDifficulty < 2.0 ) aiWeapon.Fire( 0 ); else aiWeapon.Fire( 1 ); } Pawn.SetRotation( tmprot ); } //========================================================================= //========================================================================= function bool PickTargetRelativeDestination() { if ( curSpreadState == SPREAD_APPROACH ) { if ( curTarget != none && MoveTowards( curTarget ) ) return true; // lost our target? SetSpreadState( SPREAD_COOLDOWN ); } if ( curSpreadState == SPREAD_COOLDOWN ) { if ( Super.PickTargetRelativeDestination() ) return true; if ( KeepMoving() ) return true; if ( Strafe() ) return true; if ( MoveRandomly() ) return true; } if ( curSpreadState == SPREAD_FIRING ) { SetAction( AI_WAIT, Pawn.Location ); return true; } return false; } //========================================================================= event Destroyed() { SetShieldActive( false ); } //========================================================================= DefaultProperties { aiWeaponClass = class'BlastroWeaponShotgun' bKeepPlayerInSight = false MaxPreferredDistance = 1600.0 MinPreferredDistance = 800.0 DecisionRate = 0.25 FireRate = .5//.3//0.15 MinFireRange = 0.0 MaxFireRange = 500.0 SpreadCooldown = 12.0 SpreadWarmup = 1.5 SpreadRotation = 1024 SpreadNumTrails = 8 SpreadTrail = 8 SpreadFireRange = 500 curSpreadState = SPREAD_COOLDOWN // Difficulty settings EasyFireRate = 0.55; HardFireRate = 0.35; InsaneFireRate = 0.10; EasyTrails = 6; HardTrails = 10; InsaneTrails = 14; EasyRotation = 1024; HardRotation = 1024; InsaneRotation = 512; EasyCooldown = 14.0; HardCooldown = 7.0; InsaneCooldown = 2.0; }
class LaserGuardbotAI extends BlastroAI; const WAIT_TICKS = 9; const MOVE_TICKS = 2; var int counter; enum LaserState { LASER_STATE_MOVING, LASER_STATE_WAITING, LASER_STATE_CHARGING, LASER_STATE_STOPPING }; var LaserState curState; var float stateEndTime; var float StopTime; var float NormalWaitTime; var float PostFireWaitTime; var float DartMoveTime; var float ChargeTime; var float EasyChargeTime; var float HardChargeTime; var float InsaneChargeTime; var float EasyWaitTime; var float HardWaitTime; var float InsaneWaitTime; var float EasyStopTime; var float HardStopTime; var float InsaneStopTime; //========================================================================= //========================================================================= function AdjustToCurrentDifficulty() { Super.AdjustToCurrentDifficulty(); ChargeTime = DifficultyLerp( EasyChargeTime, default.ChargeTime, HardChargeTime, InsaneChargeTime ); NormalWaitTime = DifficultyLerp( EasyWaitTime, default.NormalWaitTime, HardWaitTime, InsaneWaitTime ); PostFireWaitTime = NormalWaitTime * ( default.PostFireWaitTime / default.NormalWaitTime ); StopTime = DifficultyLerp( EasyStopTime, default.StopTime, HardStopTime, InsaneStopTime ); } //========================================================================= //========================================================================= function AIFire() { aiWeapon.CancelFire(); aiWeapon.Fire( 0 ); } //========================================================================= //========================================================================= function FireTrace() { local Rotator newFacing; if ( curTarget == none ) return; newFacing = Pawn.DesiredRotation; newFacing.Yaw += Rand( 4192 ) - 4192 / 2; Pawn.SetRotation( newFacing ); Pawn.SetDesiredRotation( newFacing, true, false, 0.0, false ); aiWeapon.Fire( 1 ); } //========================================================================= //========================================================================= function bool IsPlayerTargetable() { local Vector disp; local float distSq; if ( ownPawn == none ) return false; // Do we see our target? if ( curTarget != none && FastTrace( curTarget.Location, Pawn.Location ) ) { disp = curTarget.Location - Pawn.Location; distSq = disp dot disp; // Is our target close enough? if ( distSq >= MinFireRange * MinFireRange && distSq <= MaxFireRange * MaxFireRange ) return true; } return false; } //========================================================================= //========================================================================= function AILogicTick() { bKeepPlayerInSight = true; if ( ownPawn == none ) return; // Move about as far in one step as it takes to make our next decision moveStepDist = ownPawn.GroundSpeed * DecisionRate * OVERMOVE_RATIO * ownPawn.MaxDesiredSpeed; //if ( lastMoveVec dot lastMoveVec == 0.0 ) lastMoveVec.X = 1.0; ReassessTarget(); if ( WorldInfo.TimeSeconds > stateEndTime ) { // Move to next state switch ( curState ) { case LASER_STATE_MOVING: curState = LASER_STATE_STOPPING; stateEndTime = WorldInfo.TimeSeconds + StopTime; SetAction( AI_WAIT, Pawn.Location ); break; case LASER_STATE_STOPPING: if ( IsPlayerTargetable() ) { FireTrace(); curState = LASER_STATE_CHARGING; stateEndTime = WorldInfo.TimeSeconds + ChargeTime; } else { curState = LASER_STATE_WAITING; stateEndTime = WorldInfo.TimeSeconds + NormalWaitTime; } break; case LASER_STATE_CHARGING: AIFire(); curState = LASER_STATE_WAITING; stateEndTime = WorldInfo.TimeSeconds + PostFireWaitTime; break; case LASER_STATE_WAITING: Pawn.LockDesiredRotation( false, true ); // Play "dash" sound // PlaySound( SoundCue'BlastroSFX.Scifi_Passby1_Cue' ); if ( curTarget != none && PickTargetRelativeDestination() ) { } else MoveRandomly(); curState = LASER_STATE_MOVING; stateEndTime = WorldInfo.TimeSeconds + DartMoveTime * ( Rand( 40 ) + 80 ) * 0.01; break; } } else { // Still in current state switch ( curState ) { case LASER_STATE_MOVING: if ( recentlyBumped ) { stateEndTime = WorldInfo.TimeSeconds; curDecisionRate = 0.01; // TODO - bounce if recentlybumped break; } if ( !IsPlayerTargetable() ) bKeepPlayerInSight = false; if ( !KeepMoving() ) { stateEndTime = WorldInfo.TimeSeconds; curDecisionRate = 0.01; } break; case LASER_STATE_STOPPING: case LASER_STATE_CHARGING: case LASER_STATE_WAITING: SetAction( AI_WAIT, Pawn.Location ); break; } } recentlyBumped = false; destDisp = curDestination - Pawn.Location; destDisp.Z = 0; lastMoveVec = Normal( destDisp ); SetTimer( curDecisionRate, false, 'AILogicTick' ); curDecisionRate = DecisionRate; } //========================================================================= //========================================================================= DefaultProperties { aiWeaponClass = class'BlastroWeaponLaser' curState = LASER_STATE_WAITING StopTime = 0.5 NormalWaitTime = 1.0 PostFireWaitTime = 0.625//0.5 DartMoveTime = 0.5//2.0; ChargeTime = 0.6; stateEndTime = -1 bKeepPlayerInSight = true MaxPreferredDistance = 1100.0 MinPreferredDistance = 200.0 DecisionRate = 0.10 FireRate = 0.0 MinFireRange = 0.0 MaxFireRange = 1300.0 counter = 0; // Difficulty settings EasyFireRate = 0.0; HardFireRate = 0.0; InsaneFireRate = 0.0; EasyChargeTime = 0.8; HardChargeTime = 0.4; InsaneChargeTime = 0.3; EasyWaitTime = 1.2; HardWaitTime = 0.8; InsaneWaitTime = 0.2; EasyStopTime = 0.5; HardStopTime = 0.4; InsaneStopTime = 0.1; }