Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Windows-Phone-7

Using NPC and NPCManager to create NPC and Custom Ations in XNA, WPXNA (19)

5.00/5 (1 vote)
27 Aug 2013CPOL3 min read 16K  
Using NPC and NPCManager to create NPC and custom actions in XNA, WPXNA (19)

The original post can be found here.

Introduction/Catalog

I have developed some games on Windows Phone. Here, I'll share my experiences and gradually upload some classes, no good name, I just call it WPXNA. (Some example code may not be stringent enough.)

  • NPC
  • NPCManager
  • NPCAction
  • Example

NPC

NPC is the important content in the game, that is non-player controlled units. Therefore, I created NPC class and other related classes. Here are some fields of NPC class.

Static fields InjuredSoundName and DeadSoundName are the sounds of the NPC. And all the NPCs will use this sound.

Injured event said that NPC was injured, in this case you can let NPC show the effect of injuries.

Field FrameCount is used to calculate the time.

C#
internal static string InjuredSoundName;
internal static string DeadSoundName;

internal event EventHandler<NPCEventArgs> Injured;

private long frameCount = 0;

protected readonly List<NPCAction> actions = new List<NPCAction> ( );
protected readonly List<NPCAction> loopActions = new List<NPCAction> ( );
private int currentActionIndex;
protected int life;
protected int protoLife;
internal bool IsDied = false;

internal NPCManager Manager;

protected NPC ( IPlayScene scene, int type, Vector2 location, string movieName, 
  string extendMovieName, float speed, int angle, HitArea hitArea, int width, 
  int height, int life, IList<NPCAction> actions, double destroySecond, 
  bool isMovieRotable, bool isAreaLimited, bool isAreaEntered, double areaSecond )
 : base ( scene, type, location, movieName, extendMovieName, speed, angle, 
   hitArea, width, height, destroySecond, isMovieRotable, isAreaLimited, isAreaEntered, areaSecond )
{

 this.life = life <= 0 ? 1 : life;
 this.protoLife = this.life;

 if ( null != actions )
  this.actions.AddRange ( actions );

 this.currentActionIndex = 0;
}
  • Field actions and loopActions are used to hold actions, currentActionIndex represents the current index of the actions.
  • Field life is current HP of the NPC, field protoLife is the original HP of NPC.
  • Field IsDied indicates whether the NPC has died. Field Manager said NPCManager.

Classes that inherit from the NPC can modify the method execute, so they can complete their custom actions, such as: the enemy fighters fire bullets after 1 second.

In method updating, we will judge the execution time of the action, and determine whether these actions can repeat.

C#
protected virtual void execute ( NPCAction action )
{ }

protected override void updating ( GameTime time )
{
 this.frameCount++;

 foreach ( NPCAction action in this.loopActions.ToArray() )
  if ( action.FrameIndex <= this.frameCount )
  {
   this.execute ( action );

   if ( !action.Next ( ) )
    this.loopActions.Remove ( action );

  }

 for ( int index = this.currentActionIndex; index < this.actions.Count; index++ )
 {
  NPCAction action = this.actions[ index ];

  if ( action.FrameIndex > this.frameCount )
   break;
  else
  {
   this.execute ( action );

   if ( action.IsLoop )
   {
    this.loopActions.Add ( action );
    action.Next ( );
   }

   this.currentActionIndex++;
  }

 }

 base.updating ( time );
}

Injure method will deduct the HP of NPC, if HP is less than or equal to 0, so we will play NPC's death movie, but we'll not call Destroy method, but in the movieEnded method to determine whether the death movie has finished, we will call Destroy method there.

Derived classes can modify the method dying to add some code that will be executed before the NPC was killed.

C#
protected override void movieEnded ( object sender, MovieEventArgs e )
{

 if ( e.SequenceName == "dead" )
  this.Destroy ( );

}

protected virtual void dying ( )
{ }

public virtual bool Injure ( int life, int type )
{

 if ( this.IsDied )
  return true;

 this.scene.AudioManager.PlaySound ( InjuredSoundName );
 
 int injuredLife;

 if ( this.life < life )
  injuredLife = this.life;
 else
  injuredLife = life;

 this.life -= injuredLife;

 if ( null != this.Injured )
  this.Injured ( this, new NPCEventArgs ( this, injuredLife, type ) );

 if ( this.life <= 0 )
 {
  this.scene.AudioManager.PlaySound ( DeadSoundName );
  
  this.IsDied = true;
  this.PlayMovie ( "dead" );

  this.dying ( );
 }
 else
  this.PlayExtendMovie ( "flash" );

 return this.life <= 0;
}

NPCManager

NPCManager class is derived from class SpiritManager<T>.

C#
internal class NPCManager
 : SpiritManager<NPC>
{
 protected long frameCount = 0;

 internal event EventHandler<SpiritEventArgs> Destroyed;

 internal event EventHandler<NPCEventArgs> Injured;

 internal NPCManager ( )
  : base ( )
 { }

 protected override void spiritDestroyed ( object sender, SpiritEventArgs e )
 {

  if ( null != this.Destroyed )
   this.Destroyed ( sender, e );

  NPC npc = sender as NPC;
  npc.Injured -= this.Injured;
  npc.Manager = null;
  base.spiritDestroyed ( sender, e );
 }


 internal override void Append ( NPC spirit, int order )
 {

  spirit.Manager = this;
  spirit.Injured += this.Injured;
  base.Append ( spirit, order );
 }

 internal override void Update ( GameTime time )
 {
  this.frameCount++;
 }

 internal List<NPC> HitTest ( HitArea area )
 {
  List<NPC> npcs = new List<NPC> ( );

  foreach ( NPC npc in this.Spirits )
   if ( !npc.IsDied && area.HitTest ( npc.HitArea ) )
    npcs.Add ( npc );

  return npcs;
 }

 internal List<NPC[]> HitTest ( )
 {
  List<NPC[]> npcs = new List<NPC[]> ( );

  for ( int index1 = 0; index1 < this.Spirits.Count; index1++ )
  {
   NPC npc1 = this.Spirits[ index1 ];

   if ( npc1.IsDied )
    continue;

   for ( int index2 = index1 + 1; index2 < this.Spirits.Count; index2++ )
   {
    NPC npc2 = this.Spirits[ index2 ];

    if ( !npc2.IsDied && npc1.HitArea.HitTest ( npc2.HitArea ) )
     npcs.Add ( new NPC[] { npc1, npc2 } );

   }

  }

  return npcs;
 }

 internal void SetReelSpeed ( DirectionType direction, float speed )
 {

  foreach ( NPC npc in this.Spirits )
   npc.SetReelSpeed ( direction, speed );

 }

}

Injured and Destroyed events are used to tell the outside world NPC has been destroyed or injured, you can add points in these events for the players.

Method spiritDestroyed will be called after NPC has been destroyed, in this method, we removed the Injured event, and Destroyed event will remove in the base class.

The first HitTest method receives a parameter named area, we will have to decide which NPC hit this area, and then we return the NPC as a list. While the second HitTest method, we test collisions with NPC, such as: we need to detect collisions between vehicles in a racing game.

Method SetReelSpeed is used to set scroll speed for all the NPC.

NPCAction

After each NPC was created, they are likely to change their current status. Therefore, I created class NPCAction that includes a few pieces of information, and shows you how to change the status.

Field frameIndex, IsLoop, loopedCount, intervalFrameCount are used to indicate whether the action can be repeated, first performance time and frequency.

When the action is executed once, we will call the Next method to get the next execution time.

C#
internal abstract class NPCAction
{

 internal readonly int Type;

 private long frameIndex;
 internal long FrameIndex
 {
  get { return this.frameIndex; }
 }

 internal readonly bool IsLoop;

 private readonly int loopCount;

 private int loopedCount = 0;
 internal int LoopedCount
 {
  get { return this.loopedCount; }
 }


 private readonly long intervalFrameCount;

 protected NPCAction ( NPCAction action )
 {
  this.Type = action.Type;

  this.frameIndex = action.frameIndex;

  this.IsLoop = action.IsLoop;
  this.loopCount = action.loopCount;
  this.intervalFrameCount = action.intervalFrameCount;
 }
 protected NPCAction ( int type, float second, float intervalSecond, int loopCount )
 {
  this.Type = type;

  this.frameIndex = World.ToFrameCount ( second );

  this.IsLoop = intervalSecond > 0;
  this.loopCount = loopCount;
  this.intervalFrameCount = World.ToFrameCount ( intervalSecond );
 }

 internal virtual bool Next ( )
 {

  if ( !this.IsLoop || ( this.loopCount > 0 && this.loopedCount >= this.loopCount ) )
   return false;

  this.frameIndex += this.intervalFrameCount;
  this.loopedCount++;
  return true;
 }

 internal abstract NPCAction Clone ( );

}

Example

SceneT20 is an extension of SceneT19, in addition to the functions of SceneT19, we will also create some NPC which will automatically rotate.

Class MyNPC inherited from NPC, methods execute can be used to perform a custom action which is MyNPCAction. OffsetAngle field represents the angle of rotation.

C#
internal class MyNPC
 : NPC
{

 internal MyNPC ( IPlayScene scene, Vector2 location, int angle, IList<NPCAction> actions )
  : base ( scene, 1, location,
  "mynpc", null,
  3f, angle,
  new SingleRectangleHitArea ( new Rectangle ( -20, -20, 40, 40 ) ),
  40, 40,
  1,
  actions,
  0, true, true, true, 0 )
 { this.isMoving = true; }

 protected override void execute ( NPCAction action )
 {
  MyNPCAction myAction = action as MyNPCAction;

  this.Angle += myAction.OffsetAngle;
 }

 protected override void updateSpeed ( )
 {
  this.xSpeed = Calculator.Cos ( this.angle ) * this.speed;
  this.ySpeed = Calculator.Sin ( this.angle ) * this.speed;

  base.updateSpeed ( );
 }

 protected override void move ( )
 {
  this.Location.X += this.xSpeed;
  this.Location.Y += this.ySpeed;
 }

}

internal class MyNPCAction
 : NPCAction
{
 internal readonly int OffsetAngle;

 internal MyNPCAction ( MyNPCAction action )
  : base ( action )
 {
  this.OffsetAngle = action.OffsetAngle;
 }
 internal MyNPCAction ( int offsetAngle )
  : base ( 1, 0f, 0.5f, 10 )
 { this.OffsetAngle = offsetAngle; }

 internal override NPCAction Clone ( )
 { return new MyNPCAction ( this ); }

}

Next, we added a NPCManager to SceneT20.

C#
private NPCManager npcManager;

Finally, we create a new MyNPC in the method goButtonSelected.

C#
private void goButtonSelected ( object sender, ButtonEventArgs e )
{
 this.bulletManager.Append ( new MyBullet ( this, new Vector2 ( 10, 10 ), 45 ) );
 this.itemManager.Append ( new MyItem ( this, new Vector2 ( 420, 30 ), 135 ) );

 this.npcManager.Append ( new MyNPC ( this, new Vector2 ( 420, 420 ), 100,
  new NPCAction[] {
  new MyNPCAction ( 20 )
  }
  ) );
}

Get the code here. For more contents, please visit WPXNA.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)