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.
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.
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.
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>
.
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.
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.
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
.
private NPCManager npcManager;
Finally, we create a new MyNPC
in the method goButtonSelected
.
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.