//--------------------------------------------------------------------------------------------------------------------------------------------------- // // Copyright (C)2007 DarkWynter Studios. All rights reserved. // //--------------------------------------------------------------------------------------------------------------------------------------------------- // {License Information: Creative Commons} //--------------------------------------------------------------------------------------------------------------------------------------------------- namespace ElementalGame { #region Using Statements using System; using System.Collections; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; using System.Xml; using System.Diagnostics; #endregion //AI State public enum AIState { IDLE, GOTO, ATTACK, START_EVADE, EVADE, SEARCH_MODE}; public enum AIAttackState { PURSUE, DESTROY, NONE }; public class AI : Player { Random rand = new Random(); public const int ATTACK_RADIUS = 100; public const float HYSTERESIS_AMOUNT = 5.0f; //range public const int BOT_SPAWN_HEIGHT = 500; public const int LOWHEALTH = 50; public const int LOWMANNA = 50; private const int MAX_SEARCH_SECONDS = 5; private const int JUMP_DELAY = 1; private enum AmountType { LOW, HIGH, NEUTRAL }; private class PerceptInterface { public Mass.TerrainType hitTerrain; public Mass.ObjectType upcomingTerrain; public bool hitLeftByParticle; public bool hitRightByParticle; public List playersInView = new List(); public AmountType mannaAmount; public AmountType healthAmount; public void init() { hitTerrain = Mass.TerrainType.NONE; hitLeftByParticle = false; hitRightByParticle = false; playersInView.Clear(); mannaAmount = AmountType.NEUTRAL; healthAmount = AmountType.NEUTRAL; upcomingTerrain = Mass.ObjectType.NONE; } }; // Stimuli for the AI to react to PerceptInterface percept = new PerceptInterface(); // Number between 1 and 10 for smartness (10 being super smart) public int intelligence = 8; //Actuator properties private enum TurnDirection { LEFT, RIGHT, NOT_SET }; TurnDirection turnDirection = TurnDirection.NOT_SET; AIState aiState = new AIState(); AIAttackState aiAttackState = new AIAttackState(); Vector3 randomDestination = new Vector3(); private Stopwatch searchTimer = new Stopwatch(); private Stopwatch jumpTimer = null; private Player playerToAttack = null; private Player playerToAvoid = null; public static int AI_MAX_HEALTH = 100; public AI(int playerNumber) : base(playerNumber, ObjectLibrary.darkMatter[ObjectLibrary.darkMatterIterator++]) { mass.objectType = Mass.ObjectType.PLAYER; mass.gameObjectPointer = this; } public new void SpawnPlayer() { base.SpawnPlayer(); health = AI_MAX_HEALTH; } public override void Load(XmlNode node) { //base.LoadGame(graphics, content); health = AI_MAX_HEALTH; manna = 100; base.Load(node); characterType = CharacterType.SEPTASOUL; if (GameFlow.gameType == GameFlow.GameType.SURVIVAL) { // Enters into search mode to see who shot him aiState = AIState.SEARCH_MODE; // Only search for a few seconds.. searchTimer = Stopwatch.StartNew(); } else { aiState = AIState.IDLE; } aiAttackState = AIAttackState.NONE; //relies on the x,z being set somewhere else.. SetSpawnPoint(Vector3.Zero); } private float AngleToDirection(Vector3 direction) { Vector2 diff2 = new Vector2(direction.X, direction.Z); diff2.Normalize(); Vector2 N = new Vector2(mass.normalVector.X, mass.normalVector.Z); N.Normalize(); return (float)Math.Acos((double)(Vector2.Dot(diff2, N))); } private TurnDirection DirectionToOrientation(Vector3 direction) { // Determine which direction to turn Vector3 toDirection = new Vector3(direction.X, 0, direction.Z); toDirection.Normalize(); Vector3 lookAt = new Vector3(mass.normalVector.X, 0, mass.normalVector.Z); lookAt.Normalize(); Vector3 cross = Vector3.Cross(toDirection, lookAt); if (cross.Y < 0.2) { return TurnDirection.LEFT; } else { return TurnDirection.RIGHT; } } private void MoveTowardDirection(Vector3 direction) { float angle = AngleToDirection(direction); if (float.IsNaN(angle) || angle < 0.1) { controllerInput.playerRotation.X = 0.0f; controllerInput.playerMotion.Y = 1.0f; } else { if (turnDirection == TurnDirection.NOT_SET) { // Determine which direction to turn turnDirection = DirectionToOrientation(direction); } if (turnDirection == TurnDirection.LEFT) { controllerInput.playerRotation.X = 0.05f; } else { controllerInput.playerRotation.X = -0.05f; } } } private List GetPlayersInView(ObjectLibrary objectLibrary) { List playersInView = new List(); BoundingFrustum viewFrustum; Matrix matrixModelView = Matrix.CreateLookAt(mass.currentPosition, mass.currentPosition + mass.normalVector, mass.upVector); // Create a limited range projection matrix for distance culling Matrix limitedMatrixProjection = Matrix.CreatePerspectiveFieldOfView((float)Math.PI / 4, (1.0f * XML.SystemSettings.GAME_WINDOW_WIDTH) / XML.SystemSettings.GAME_WINDOW_HEIGHT, 0.3f, 1000.0f * (1.0f * intelligence / 2.0f)); viewFrustum = new BoundingFrustum(matrixModelView * limitedMatrixProjection); // Check for human in radius for (int i = 0; i < objectLibrary.humans.Count; i++) { if (objectLibrary.humans[i].IsAlive()) { // Player found! // Is he in view? if (viewFrustum.Contains(objectLibrary.humans[i].boundingSphere) == ContainmentType.Contains || viewFrustum.Contains(objectLibrary.humans[i].boundingSphere) == ContainmentType.Intersects) { playersInView.Add(objectLibrary.humans[i]); } } } // Check for AI in radius // If in survival mode.. ignore other AI bots // Also, don't attack other bots unless there are no human players visible if (GameFlow.gameType != GameFlow.GameType.SURVIVAL && GameFlow.gameType != GameFlow.GameType.STORY_MODE && playersInView.Count == 0) { for (int i = 0; i < objectLibrary.bots.Count; i++) { if (objectLibrary.bots[i].IsAlive() && objectLibrary.bots[i] != this) { //Player found! //is he in view? if (viewFrustum.Contains(objectLibrary.bots[i].boundingSphere) == ContainmentType.Contains || viewFrustum.Contains(objectLibrary.bots[i].boundingSphere) == ContainmentType.Intersects) { playersInView.Add(objectLibrary.bots[i]); } } } } return playersInView; } private Player GetClosestPlayerInView() { // Get the closest player in the playersInView array if (percept.playersInView.Count == 0) { return null; } Player closestPlayer = null; float closestDistance = 100000.0f; // Check for AI in radius for (int i = 0; i < percept.playersInView.Count; i++) { float distance = (percept.playersInView[i].mass.currentPosition - mass.currentPosition).Length(); if (distance < closestDistance) { closestPlayer = percept.playersInView[i]; closestDistance = distance; } } return closestPlayer; } private bool DoSmarterThing() { int probability = rand.Next(1000); return (probability < (intelligence * 100) - HYSTERESIS_AMOUNT); } private bool DoDumberThing() { int probability = rand.Next(1000); return (probability > (intelligence * 100) + HYSTERESIS_AMOUNT); } private void Jump() { if (jumpTimer == null) { jumpTimer = Stopwatch.StartNew(); } if (jumpTimer.Elapsed.Seconds >= JUMP_DELAY) { controllerInput.jump = true; jumpTimer = Stopwatch.StartNew(); } } public void UpdatePercepts(ObjectLibrary objectLibrary) { // Clear out percepts first percept.init(); // First check for all stimuli // Was he hit by something? if (hitWhere.Contains(HitWhere.LEFT)) { percept.hitLeftByParticle = true; } if (hitWhere.Contains(HitWhere.RIGHT)) { percept.hitRightByParticle = true; } if (mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_HIGH) || collisionSensor.mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_HIGH)) { percept.hitTerrain = Mass.TerrainType.TERRAIN_HIGH; } else if (mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_MEDIUM) || collisionSensor.mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_MEDIUM)) { percept.hitTerrain = Mass.TerrainType.TERRAIN_MEDIUM; } else if (mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_LOW) || collisionSensor.mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_LOW)) { percept.hitTerrain = Mass.TerrainType.TERRAIN_LOW; } else if (mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_GROUND) || collisionSensor.mass.objectHitBy.Contains(Mass.TerrainType.TERRAIN_GROUND)) { percept.hitTerrain = Mass.TerrainType.TERRAIN_GROUND; } //Find all players in view percept.playersInView = GetPlayersInView(objectLibrary); if (health < LOWHEALTH - HYSTERESIS_AMOUNT) { percept.healthAmount = AmountType.LOW; } else if (health > LOWHEALTH + HYSTERESIS_AMOUNT) { percept.healthAmount = AmountType.HIGH; } else { percept.healthAmount = AmountType.NEUTRAL; } if (manna < LOWMANNA - HYSTERESIS_AMOUNT) { percept.mannaAmount = AmountType.LOW; } else if (manna > LOWMANNA + HYSTERESIS_AMOUNT) { percept.mannaAmount = AmountType.HIGH; } else { percept.mannaAmount = AmountType.NEUTRAL; } } // Functions for updating the AI's current state private void UpdateState() { if (aiState == AIState.IDLE) { // Find a new place to go // just always do it for now //if (DoSmarterThing() == true) { randomDestination = new Vector3(rand.Next(0, Terrain.collisionMapSize), XML.PlayerSettings.DEFAULT_PLAYER_HEIGHT, rand.Next(0, Terrain.collisionMapSize)); turnDirection = TurnDirection.NOT_SET; aiState = AIState.GOTO; } } else if (aiState == AIState.GOTO) { // Move towards a destination point Vector3 diff = new Vector3(randomDestination.X - mass.currentPosition.X, 0, randomDestination.Z - mass.currentPosition.Z); MoveTowardDirection(diff); if (Math.Abs(diff.X) < 5 && Math.Abs(diff.Z) < 5) { aiState = AIState.IDLE; } } else if (aiState == AIState.SEARCH_MODE) { // Search in the specified direction until the time runs out if (turnDirection == TurnDirection.LEFT) { controllerInput.playerRotation.X = -0.1f; } else if (turnDirection == TurnDirection.RIGHT) { controllerInput.playerRotation.X = 0.1f; } if (searchTimer.Elapsed.Seconds > MAX_SEARCH_SECONDS) { aiState = AIState.IDLE; } } else if (aiState == AIState.ATTACK) { if (playerToAttack != null) { Vector3 vecToPlayer = playerToAttack.mass.currentPosition - mass.currentPosition; if (aiAttackState == AIAttackState.NONE) { turnDirection = TurnDirection.NOT_SET; aiAttackState = AIAttackState.PURSUE; } else if (aiAttackState == AIAttackState.PURSUE) { MoveTowardDirection(vecToPlayer); float angle = AngleToDirection(vecToPlayer); // NaN if he is facing you if (float.IsNaN(angle) || angle < 0.1) { if (vecToPlayer.Length() <= ATTACK_RADIUS) { AttackPlayer(); aiAttackState = AIAttackState.DESTROY; } } } else if (aiAttackState == AIAttackState.DESTROY) { //controllerInput.attackAmount = 1.0f; controllerInput.playerMotion.Y = 0.0f; turnDirection = DirectionToOrientation(vecToPlayer); if (turnDirection == TurnDirection.LEFT) { controllerInput.playerRotation.X = -0.1f; } else if (turnDirection == TurnDirection.RIGHT) { controllerInput.playerRotation.X = 0.1f; } float angle = AngleToDirection(vecToPlayer); if (float.IsNaN(angle) || angle < 0.1f) { controllerInput.playerRotation.X = 0.0f; } // If the player moves away then face and pursue again if (vecToPlayer.Length() > ATTACK_RADIUS) { turnDirection = TurnDirection.NOT_SET; aiAttackState = AIAttackState.PURSUE; } else { //in range AttackPlayer(); } } //If hit while attacking... if (percept.hitLeftByParticle || percept.hitRightByParticle) { controllerInput.playerMotion.Y = -1.0f; } } else { // Reset all controller inputs controllerInput.playerMotion = Vector2.Zero; controllerInput.playerRotation = Vector2.Zero; controllerInput.attackAmount = 0.0f; controllerInput.defendAmount = 0.0f; controllerInput.elementUse = 0; aiState = AIState.IDLE; } } else if (aiState == AIState.START_EVADE) { if (playerToAttack != null) { Vector3 vecToPlayer = playerToAttack.mass.currentPosition - mass.currentPosition; if (vecToPlayer.Length() < ATTACK_RADIUS - HYSTERESIS_AMOUNT) { turnDirection = TurnDirection.NOT_SET; playerToAvoid = playerToAttack; aiState = AIState.EVADE; } else if (vecToPlayer.Length() > ATTACK_RADIUS + HYSTERESIS_AMOUNT) { //Player is too far away.. ignore aiState = AIState.IDLE; } } else { aiState = AIState.IDLE; } } else if (aiState == AIState.EVADE) { if (playerToAvoid != null) { Vector3 vecToPlayer = playerToAvoid.mass.currentPosition - mass.currentPosition; MoveTowardDirection(-vecToPlayer); if (vecToPlayer.Length() > ATTACK_RADIUS) { aiState = AIState.IDLE; } //If they were also hit by something if (percept.hitLeftByParticle || percept.hitRightByParticle) { Jump(); } } else { aiState = AIState.IDLE; } } } private void AttackPlayer() { //pick random element int element = rand.Next(3); switch (element) { case 0: controllerInput.modeSwitch = GameController.ElementalMode.EARTH_MODE; break; case 1: controllerInput.modeSwitch = GameController.ElementalMode.WATER_MODE; break; case 2: controllerInput.modeSwitch = GameController.ElementalMode.WIND_MODE; break; } controllerInput.attackMode = GameController.AttackMode.ATTACK; controllerInput.attackAmount = 1.0f; particleMassValue = rand.Next(50); particleThermalValue = rand.Next(50); } // General update function public override void Update(ObjectLibrary objectLibrary) { //if dead they can't update if (IsAlive() == false) { return; } // Reset all controller inputs controllerInput.init(); // Turn terrain mod back off terrainModEnabled = false; // Fuzzy state machine // * <- means smarter choice // Update percepts UpdatePercepts(objectLibrary); // Was he hit by something? if (percept.hitLeftByParticle || percept.hitRightByParticle) { if (aiState != AIState.ATTACK && aiState != AIState.EVADE) { if (percept.hitRightByParticle) { // Hit on the right turnDirection = TurnDirection.RIGHT; } else { // Hit on the left turnDirection = TurnDirection.LEFT; } // Enters into search mode to see who shot him aiState = AIState.SEARCH_MODE; // Only search for a few seconds.. searchTimer = Stopwatch.StartNew(); } } // Now look for players within view else if (percept.playersInView.Count > 0) { playerToAttack = GetClosestPlayerInView(); Vector3 toPlayer = playerToAttack.mass.currentPosition - mass.currentPosition; controllerInput.playerRotation = Vector2.Zero; // Check your health if (percept.healthAmount == AmountType.LOW) { // Low health if (DoSmarterThing()) { // RUN AWAY aiState = AIState.START_EVADE; } else if (DoDumberThing()) { // ATTACK! aiState = AIState.ATTACK; } } else if (percept.healthAmount == AmountType.HIGH) { if (percept.mannaAmount == AmountType.LOW) { // Low manna if (DoSmarterThing()) { // RUN AWAY aiState = AIState.START_EVADE; } else if (DoDumberThing()) { // ATTACK! aiState = AIState.ATTACK; } } else if (percept.mannaAmount == AmountType.HIGH) { // Have manna if (DoSmarterThing()) { // ATTACK! aiState = AIState.ATTACK; } else if (DoDumberThing()) { // RUN AWAY aiState = AIState.START_EVADE; } } } } // Hit by terrain means he either fell from a high place or walked into a wall else if (percept.hitTerrain == Mass.TerrainType.TERRAIN_GROUND) { // Recalculate where to go aiState = AIState.IDLE; } else if (percept.hitTerrain == Mass.TerrainType.TERRAIN_LOW) { //terrainModEnabled = true; //controllerInput.defendAmount = -1.0f; } if (percept.hitTerrain == Mass.TerrainType.TERRAIN_MEDIUM || percept.hitTerrain == Mass.TerrainType.TERRAIN_HIGH) { Jump(); } UpdateState(); /* h4x0rz switch (aiState) { case AIState.GOTO: materialDiffuse[0] = 0; materialDiffuse[1] = 0; materialDiffuse[2] = 1; break; case AIState.EVADE: materialDiffuse[0] = 0; materialDiffuse[1] = 1; materialDiffuse[2] = 0; break; case AIState.ATTACK: if (aiAttackState == AIAttackState.NONE) { materialDiffuse[0] = 0; materialDiffuse[1] = 0; materialDiffuse[2] = 0; break; } else if (aiAttackState == AIAttackState.PURSUE) { materialDiffuse[0] = 0.4f; materialDiffuse[1] = 0; materialDiffuse[2] = 0; break; } else { materialDiffuse[0] = 1; materialDiffuse[1] = 0; materialDiffuse[2] = 0; break; } case AIState.SEARCH_MODE: materialDiffuse[0] = 1; materialDiffuse[1] = 1; materialDiffuse[2] = 0; break; case AIState.START_EVADE: materialDiffuse[0] = 0; materialDiffuse[1] = 0.5f; materialDiffuse[2] = 0; break; default: materialDiffuse[0] = 1; materialDiffuse[1] = 1; materialDiffuse[2] = 1; break; } * */ controllerInput.elementUse = controllerInput.attackAmount + controllerInput.defendAmount; base.Update(objectLibrary); // UseControllerInput(objectLibrary); } } }