Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 51 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
51
Dung lượng
6,38 MB
Nội dung
You update the player’s weapon by calling the weapon’s Update method and passing the player’s right hand bone as the weapon’s parent bone. In this way, the weapon is updated according to the player’s right hand. You also need to set the weapon’s target direction as the player’s front direction (as illustrated in Figure 12-9). Note that you need to transform the player’s right hand bone by the player’s transformation matrix before using it to update the player’s weapon. Following is the code for the player’s Update methods: public override void Update(GameTime time) { // Update the player's waist bone float elapsedTimeSeconds = (float)time.ElapsedGameTime.TotalSeconds; UpdateWaistBone(elapsedTimeSeconds); // Update player's base class // It's where the player's position and animated model are updated base.Update(time); // Update camera chase position UpdateChasePosition(); // Update player weapon Matrix transformedHand = AnimatedModel.BonesAnimation[RIGHT_HAND_BONE_ID] * Transformation.Matrix; playerWeapon.Update(time, transformedHand); playerWeapon.TargetDirection = HeadingVector + UpVector * rotateWaistBone; } Enemy The Enemy class is the one that has the enemy NPC’s logic and attributes. Figure 12-10 exhibits a spider model used as an enemy in the game. CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME 379 9241CH12.qxd 3/27/08 5:46 PM Page 379 Figure 12-10. An alien spider model. Courtesy of Psionic (http://www.psionic3d.co.uk). Differently from the player, the enemy is computer controlled, so you need to imple- ment its AI. The enemy’s AI is simple, having only four different states: Wandering, Chasing Player, Attacking Player, and Dead. Figure 12-11 shows the diagram of the AI built for the enemies. Figure 12-11. Enemy AI diagram CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME380 9241CH12.qxd 3/27/08 5:46 PM Page 380 In the AI diagram in Figure 12-11, each circle represents a different enemy state, and the arrows represent the actions that make an enemy change its state. The enemy’s AI starts in the Wandering state. In this state, the enemy keeps moving around the map ran- domly looking for the player. Whenever the enemy sees the player or gets shot by the player, he changes his state to Chasing Player. In the Chasing Player state, the enemy moves closer to the player until he is near enough to attack the player. When that hap- pens, the enemy state is altered to Attacking Player. In this state, the enemy attacks the player successively until the player dies or the player runs. If the player tries to run from the enemy, the enemy’s state is changed back to Chasing Player. Notice that once the enemy starts to chase the player, the enemy stays in a cycle between the states Chasing Player and Attacking Player, not returning to the Wandering state. Each enemy has an attribute to store his current state, among an enumeration of possible states. // Possible enemy states public enum EnemyState { Wander = 0, ChasePlayer, AttackPlayer, Dead } // Current enemy state (default = Wander) EnemyState state; For each one of the possible enemy states you’ll declare some attributes and create a method to execute this state. To control the transitions between the enemy states, you’ll overwrite the Update method of its base class. Updating the Enemy The enemy’s Update method manages the transition between the enemy states. For every arrow in the AI state diagram, shown in Figure 12-11, there must be a condition in the Update method. In the beginning of the Update method you calculate the enemy’s chaseVector, which contains the direction from the enemy’s position to the player’s position. You use the length of this vector to check the distance between the enemy and the player. Then, for CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME 381 9241CH12.qxd 3/27/08 5:46 PM Page 381 each player’s state you check if you can execute this state or need to change it to a new state. Notice that all enemies have a reference to the Player class, which is used to obtain the player’s current position. Following is the Update method’s code: public override void Update(GameTime time) { // Calculate chase vector every time chaseVector = player.Transformation.Translate – Transformation.Translate; float distanceToPlayer = chaseVector.Length(); switch (state) { case EnemyState.Wander: // Enemy perceives the player – Change state if (isHited || distanceToPlayer < perceptionDistance) state = EnemyState.ChasePlayer; else Wander(time); break; case EnemyState.ChasePlayer: // Enemy is near enough to attack – Change state if (distanceToPlayer <= attackDistance) { state = EnemyState.AttackPlayer; nextActionTime = 0; } else ChasePlayer(time); break; case EnemyState.AttackPlayer: // Player flees – Change state if (distanceToPlayer > attackDistance * 2.0f) state = EnemyState.ChasePlayer; else AttackPlayer(time); break; CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME382 9241CH12.qxd 3/27/08 5:46 PM Page 382 default: break; } base.Update(time); } Wandering In the Wandering state, the enemy walks randomly through the map, without a specific goal. To execute this action, you need to generate random positions over the map within a radius from the enemy’s actual position and make the enemy move to these positions. Following are the attributes of the Enemy class used by the Wandering state: static int WANDER_MAX_MOVES = 3; static int WANDER_DISTANCE = 70; static float WANDER_DELAY_SECONDS = 4.0f; static float MOVE_CONSTANT = 35.0f; static float ROTATE_CONSTANT = 100.0f; // Wander int wanderMovesCount; Vector3 wanderStartPosition; Vector3 wanderPosition; The WANDER_MAX_MOVES variable defines the number of random movements that the enemy makes until he returns to his initial position, and the wanderMovesCount variable stores the number of movements that the unit has already made. You can use these vari- ables to restrict the distance that the enemy could reach from his initial position, forcing him to return to his start position after a fixed number of random movements. Besides that, the WANDER_DELAY_SECONDS variable stores the delay time between each movement of the unit. The WANDER_DISTANCE variable stores the minimum distance that the unit walks in each movement, and the variables wanderStartPosition and wanderPosition store, respec- tively, the enemy’s initial position and destination while in the Wandering state. Finally, MOVE_CONSTANT and ROTATE_CONSTANT store a constant value used to move and rotate the enemy. To execute the enemy’sWandering state you’ll create the Wander method. In the Wander method, you first check if the enemy has already reached his destination position, which is stored in the wanderPosition attribute. To do that, you create a vector from the enemy’s position to his destination and use the length of this vector to check the distance CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME 383 9241CH12.qxd 3/27/08 5:46 PM Page 383 between them. If the distance is below a defined epsilon value (for example, 10.0), the enemy has reached his destination and a new destination must be generated: // Calculate wander vector on X, Z axis Vector3 wanderVector = wanderPosition - Transformation.Translate; wanderVector.Y = 0.0f; float wanderLength = wanderVector.Length(); // Reached the destination position if (wanderVector.Length() < DISTANCE_EPSILON) { // Generate a new wander position } Note that when the enemy is created, his first destination position is equal to his start position. If the number of random movements the enemy makes is lower than the maximum number of consecutive random movements that he could make, his new destination position will be a random generated position. Otherwise, the next enemy destination will be his start position. // Generate a new random position if (wanderMovesCount < WANDER_MAX_MOVES) { wanderPosition = Transformation.Translate + RandomHelper.GeneratePositionXZ(WANDER_DISTANCE); wanderMovesCount++; } // Go back to the start position else { wanderPosition = wanderStartPosition; wanderMovesCount = 0; } // Next time wander nextActionTime = (float)time.TotalGameTime.TotalSeconds + WANDER_DELAY_SECONDS + WANDER_DELAY_SECONDS * (float)RandomHelper.RandomGenerator.NextDouble(); The enemy’s random destination position is generated through the GeneratePositionXZ method of your RandomHelper class. After generating the enemy’s destination, you also CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME384 9241CH12.qxd 3/27/08 5:46 PM Page 384 generate a random time used to start moving the enemy to his new destination. Follow- ing is the complete code for the Wander method of the Enemy class: private void Wander(GameTime time) { // Calculate wander vector on X, Z axis Vector3 wanderVector = wanderPosition - Transformation.Translate; wanderVector.Y = 0.0f; float wanderLength = wanderVector.Length(); // Reached the destination position if (wanderLength < DISTANCE_EPSILON) { SetAnimation(EnemyAnimations.Idle, false, true, false); // Generate a new random position if (wanderMovesCount < WANDER_MAX_MOVES) { wanderPosition = Transformation.Translate + RandomHelper.GeneratePositionXZ(WANDER_DISTANCE); wanderMovesCount++; } // Go back to the start position else { wanderPosition = wanderStartPosition; wanderMovesCount = 0; } // Next time wander nextActionTime = (float)time.TotalGameTime.TotalSeconds + WANDER_DELAY_SECONDS + WANDER_DELAY_SECONDS * (float)RandomHelper.RandomGenerator.NextDouble(); } // Wait for the next action time if ((float)time.TotalGameTime.TotalSeconds > nextActionTime) { wanderVector *= (1.0f / wanderLength); Move(wanderVector); } } CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME 385 9241CH12.qxd 3/27/08 5:46 PM Page 385 At the end of the Wander method, you check if the time for the next wander action has arrived. In this case, you normalize the wanderVector, which contains the direction from the enemy to his destination, and makes the enemy move in this direction through the Move method. You’ll create the Move method to move the enemy from his original position using an arbitrary direction vector. You can move the enemy by setting his linear velocity as the desired direction vector, inside the Move method. Remember that the enemy’s position is updated according to his linear velocity by the Update method’s base class (TerrainUnit). While moving the unit, you also need to set its angular velocity, heading the unit in the same direction it is moving. Following is the code for the Move method: private void Move(Vector3 direction) { // Change enemy's animation SetAnimation(EnemyAnimations.Run, false, true, (CurrentAnimation == EnemyAnimations.TakeDamage)); // Set the new linear velocity LinearVelocity = direction * MOVE_CONSTANT; // Angle between heading and move direction float radianAngle = (float)Math.Acos( Vector3.Dot(HeadingVector, direction)); if (radianAngle >= 0.1f) { // Find short side to rotate // Clockwise (CW) or CCW (Counterclockwise) float sideToRotate = Vector3.Dot(StrafeVector, direction); Vector3 rotationVector = new Vector3(0, ROTATE_CONSTANT * radianAngle, 0); if (sideToRotate > 0) AngularVelocity = -rotationVector; else AngularVelocity = rotationVector; } } I n the Move method, y ou first set the linear v elocity of the enemy as its direction par ameter multiplied b y the MOVE_CONSTANT v ar iable . N ext, y ou calculate the angle betw een the enemy’ s heading v ector and its dir ection v ector.You need this angle to rotate the unit and head it in the same dir ection it is mo ving. Y ou can use the Dot method of XNA ’ s Vector3 class to get the cosine of the angle betw een the enemy’ s heading v ector and CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME386 9241CH12.qxd 3/27/08 5:46 PM Page 386 its direction vector, and the Acos method of the Math class to get the angle between these vectors from its cosine. After calculating the angle between the enemy’s heading and direction, you still need to know from which side to rotate the unit—clockwise (CW) or counterclockwise (CCW). For example, you can find that the angle between the enemy’s heading and direction is 90 degrees, but you still don’t know from which side to rotate him. You can find the correct side to rotate the enemy, calculating the cosine of the angle between the enemy’s strafe vector—which is perpendicular to the heading vector—and its direction vector. If the cosine is positive, you need to apply a negative rotation on the enemy, making him rotate clockwise; otherwise, you need to apply a positive rotation, making him rotate counterclockwise. The rotation is set as the enemy’s AngularVelocity and is multiplied by the ROTATE_CONSTANT variable. Chasing Player In the Chasing Player state, the enemy needs to move to the player’s current position. You can do this by making the enemy move through the chaseVector vector, which is the direction from the enemy to the player, and is calculated in the enemy’s Update method. Following is the code for the ChasePlayer method: private void ChasePlayer(GameTime time) { Vector3 direction = chaseVector; direction.Normalize(); Move(direction); } Attacking Player In the Attacking Player state, the enemy keeps attacking the player successively, causing damage to him. To do that, you can simply execute the ReceiveDamage method of the Player instance and wait for the next time to attack. The attributes that you need to create to handle the Attacking Player state is the delay time in seconds between each attack and the time the enemy could execute a new attack action: float nextActionTime; F ollo wing is the code for the AttackPlayer method: private void AttackPlayer(GameTime time) { float elapsedTimeSeconds = (float)time.TotalGameTime.TotalSeconds; if (elapsedTimeSeconds > nextActionTime) CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME 387 9241CH12.qxd 3/27/08 5:46 PM Page 387 { // Set attacking animation SetAnimation(EnemyAnimations.Bite, false, true, false); // Next attack time player.ReceiveDamage(attackDamage); nextActionTime = elapsedTimeSeconds + ATTACK_DELAY_SECONDS; } } Finishing the Game Engine By now you have already created all the game engine classes, helper classes, and almost all the game logic classes. What you have to do now is create a class to control the main game logic, and some classes to store and create the game levels. Besides that, you also need to create the main game class that extends the XNA’s Game class. You’ll create all these classes in the following sections. Game Level Each game level is composed of a fixed set of objects: cameras, lights, a terrain, a sky- dome, a player, and enemies. For the game levels, create a structure named GameLevel inside the GameLogic namespace. Following is the code for the GameLevel struct: public struct GameLevel { // Cameras, Lights, Terrain, and Sky public CameraManager CameraManager; public LightManager LightManager; public Terrain Terrain; public SkyDome SkyDome; // Player and Enemies public Player Player; public List<Enemy> EnemyList; } Creating the Game Levels In the XNA TPS game, you create the game levels inside the game code, instead of loading them from a file. To do that, create a static class named LevelCreator in the GameLogic CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME388 9241CH12.qxd 3/27/08 5:46 PM Page 388 [...]... gameLevel.LightManager.AmbientLightColor = new Vector3(0.1f); // Create the game lights and add them to the light manager gameLevel.LightManager.Add("MainLight", new PointLight(new Vector3 (100 00, 100 00, 100 00), new Vector3(0.2f))); gameLevel.LightManager.Add("CameraLight", new PointLight(Vector3.Zero, Vector3.One)); 9241CH12.qxd 3/27/08 5:46 PM Page 391 CHAPTER 12 s CREATING A THIRD-PERSON SHOOTER GAME // Add the light manager to the service... Enemies gameLevel.EnemyList = ScatterEnemies (game, 20, 150, 800, gameLevel.Player); 393 9241CH12.qxd 394 3/27/08 5:46 PM Page 394 CHAPTER 12 s CREATING A THIRD-PERSON SHOOTER GAME Now that you’ve created all the game level objects, your level is ready to be played GameScreen Class Now it’s time to put all the game objects and logic together in a new class named GameScreen The GameScreen is the main game. .. enemies The code used to create the player follows: // Create the player gameLevel.Player = new Player (game, UnitTypes.PlayerType.Marine); gameLevel.Player.Initialize(); gameLevel.Player.Transformation = new Transformation( new Vector3(- 210, 0, 10) , new Vector3(0, 70, 0), Vector3.One); gameLevel.Player.AttachWeapon(UnitTypes.PlayerWeaponType.MachineGun); // Player chase camera offsets gameLevel.Player.ChaseOffsetPosition... create a static method named CreateLevel to create the game levels This method needs to receive an instance of the Game class, because it uses the Game s ContentManager to load the game assets and the Game s ServicesContainer to share some game objects When the level is created, you add the CameraManager, LightManager, and Terrain to the ServiceContainer of the Game class, sharing these objects with... method of the LevelCreator class to generate the game level, which you store in the class’s gameLevel attribute Game Update The game update logic is divided into three methods: Update, UpdateInput, and UpdateWeaponTarget, where the main method called to update the game is the Update method You use the UpdateInput method to handle the user input, and the UpdateWeaponTarget method to check which enemy... tile); } Now, you create the game s sky: // Create the sky gameLevel.SkyDome = new SkyDome (game) ; gameLevel.SkyDome.Initialize(); gameLevel.SkyDome.Load("SkyDome"); gameLevel.SkyDome.TextureMaterial = GetTextureMaterial( game, "SkyDome", Vector2.One); The game s sky also has a TextureMaterial that you can create through the GetTextureMaterial method Last, you need to create the game s logic objects, which... GetTextureMaterial( game, "Rockbump", new Vector2(128, 128)); gameLevel.Terrain.Material = terrainMaterial; // Add the terrain to the service container game. Services.AddService(typeof(Terrain), gameLevel.Terrain); The terrain material is composed of a LightMaterial and some TextureMaterial After creating the terrain material, you need to set it into the terrain, and you also need to add the terrain to the game services... SHOOTER GAME gameSettings.KeyboardSettings[0])); Services.AddService(typeof(InputHelper), inputHelper); // Game Screen Components.Add(new GameScreen(this, LevelCreator.Levels.AlienPlanet)); } In the class constructor, you first set the game screen title and the root directory of the content manager Next, you read the game settings from an XML file, using the SettingsManager class, and use the game settings... games Then, based on this review, you created a basic design for your game, divided into defining the game, gameplay, and technical design parts After that, you started to develop the game code, which was divided into three main namespaces: GameBase, GameLogic, and Helpers In the GameBase namespace, you created all the classes for the game engine, some of which you created in the previous chapters Then,... open source game engines and components But if there’s a single piece of advice we can ask you to follow, it is that you don’t start by analyzing samples or by trying to collect code on the Internet to create your own game engine, including everything you might need for a game Too many people out there are creating samples, components, and game engines with XNA, and too few are creating real games, even . Vector3( 100 00, 100 00, 100 00) , new Vector3 (0. 2f))); gameLevel.LightManager.Add("CameraLight", new PointLight(Vector3.Zero, Vector3.One)); CHAPTER 12 ■ CREATING A THIRD-PERSON SHOOTER GAME3 90 924 1CH 12. qxd. positioned at ( 100 00, 100 00, 100 00) , which barely illuminates the scene, and a camera light positioned at the camera position, which highly illuminates the scene. You add these lights to the LightManager,. offsets gameLevel.Player.ChaseOffsetPosition = new Vector3 [2] ; gameLevel.Player.ChaseOffsetPosition [0] = new Vector3(3.0f, 5.0f, 0. 0f); gameLevel.Player.ChaseOffsetPosition[1] = new Vector3(3.0f, 4.0f, 0. 0f); After cr eating