• 使用 NPC,NPCManager 在 XNA 中创建 NPC(十九)


    平方已经开发了一些 Windows Phone 上的一些游戏,算不上什么技术大牛。在这里分享一下经验,仅为了和各位朋友交流经验。平方会逐步将自己编写的类上传到托管项目中,没有什么好名字,就叫 WPXNA 吧,最后请高手绕道而行吧,以免浪费时间。(为了突出重点和减少篇幅,有些示例代码可能不够严谨。)

    NPC

    NPC 是游戏中重要的内容,也就是非玩家控制单位。所以,平方创建了 NPC 类和其他相关的类。下面是 NPC 类的一些字段。

    静态字段 InjuredSoundName 和 DeadSoundName 表示 NPC 受伤的声音和死亡的声音。由于使用静态字段,所以所有的 NPC 都将使用此声音。

    事件 Injured 表示 NPC 受伤之后,你可以在这个事件中让 NPC 显示受伤的效果。

    字段 frameCount 用来计算时间,将根据 frameCount 来执行动作。

    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;
    }

    字段 actions,loopActions,currentActionIndex 都和动作相关。actions 和 loopActions 用来存放动作,currentActionIndex 表示当前动作的索引。

    字段 life 表示 NPC 当前的生命值,字段 protoLife 表示 NPC 生命的原始值。

    字段 IsDied 表示 NPC 是否已经死亡。字段 Manager 表示 NPC 管理器。

    继承自 NPC 的类可以修改方法 execute,这样就可以完成他们的自定义动作,比如:敌人的战斗机在 1 秒后发射子弹。

    在 updating 方法中,我们将会判断动作的执行时间,如果可以则执行这些动作,并判断这些动作是否可以重复执行,如果可以则将动作添加到 loopActions 字段中。

    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 将扣除 NPC 的生命值,如果生命值小于等于 0,那么我们将播放 NPC 死亡的电影,但是并不会调用 Destroy 方法,而是在 movieEnded 方法中判断死亡电影是否播放完毕,在播放完毕后我们才会调用 Destroy 方法。

    派生类可以修改 dying 方法来添加一些代码,这些代码将在 NPC 死亡前执行。

    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;
    }

    NPC 管理器

    NPCManager 类派生自类 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 );
    
        }
    
    }

    事件 Destroyed 和 Injured 用来通知外界 NPC 已经被摧毁或者受伤,你可以在这些事件中为玩家加分。

    方法 spiritDestroyed 会在 NPC 被摧毁后执行,在这个方法中,我们移除了 NPC 的 Injured 事件,而在基类中会移除 Destroyed 事件。

    第一个 HitTest 方法接收了一个 HitArea 类型的参数 area,我们将判断哪些 NPC 和 area 发生了碰撞,然后将这些 NPC 作为列表返回。而第二个 HitTest 方法,我们测试互相碰撞的 NPC 并返回,比如:在竞速类游戏中,我们需要检测车辆之间的碰撞。

    方法 SetReelSpeed 用来设置所有 NPC 的卷轴速度。

    NPC 动作

    每一个 NPC 在被创建之后,都可能会改变自己当前的状态。因此,平方创建了类 NPCAction,他包含了一些信息,这些信息说明了如何改变 NPC 的状态。

    有时候要完成一个动作还需要其他的类,所以使用字段 Type 来说明动作的类型。字段 frameIndex,IsLoop,loopedCount,intervalFrameCount 用来说明动作是否可以被重复执行,第一次执行的时间,以及频率,执行次数。

    当动作执行一次之后,将调用 Next 方法来获取下一次的执行时间,或者判断是否可以继续执行。

    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 ( );
    
    }

    示例

    场景 SceneT20 是 SceneT19 的扩展,除了有 SceneT19 的功能,还将创建一些 NPC,这些 NPC 将自动旋转。

    类 MyNPC 继承自 NPC,方法 execute 可以用来执行自定义动作,也就是 MyNPCAction。类 MyNPCAction 的 OffsetAngle 字段表示旋转角度。

    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 ); }
    
    }

    接下来,我们为 SceneT20 增加了一个 NPCManager。

    private NPCManager npcManager;

    最后,我们在 goButtonSelected 方法增加了代码,创建一个新的 MyNPC。

    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 )
            }
            ) );
    }

    本期视频 http://v.youku.com/v_show/id_XNjAxNDczMTIw.html

    项目地址 http://wp-xna.googlecode.com/

    更多内容 WPXNA
    平方开发的游戏 http://zoyobar.lofter.com/
    QQ 群 213685539

    欢迎访问我在其他位置发布的同一文章:http://www.wpgame.info/post/decc4_858ce9



  • 相关阅读:
    Velocity Obstacle
    游戏AI技术 2
    游戏AI技术
    状态同步
    Realtime Rendering 1.1
    Steering Behaviors
    Realtime Rendering 6
    网络同步
    War3编辑器
    Realtime Rendering 5
  • 原文地址:https://www.cnblogs.com/zoyobar/p/wpxna19.html
Copyright © 2020-2023  润新知