使用 Spirit 类在 XNA 中创建游戏中的基本单位精灵(十三)
平方已经开发了一些 Windows Phone 上的一些游戏,算不上什么技术大牛。在这里分享一下经验,仅为了和各位朋友交流经验。平方会逐步将自己编写的类上传到托管项目中,没有什么好名字,就叫 WPXNA 吧,最后请高手绕道而行吧,以免浪费时间。(为了突出重点和减少篇幅,有些示例代码可能不够严谨。)
Spirit
如果你觉得不习惯,可以使用精灵的另一种写法 Sprite。Spirit 是一个重要的类,表示游戏中的单位,比如:敌人,玩家等。很多类都会从 Spirit 类派生。
下面是 Spirit 的一些成员。
Destroyed 事件会在 Destroy 方法中被调用,而这个事件主要被精灵管理器所使用,精灵管理器会在以后被讲到。DrawOrderChanged 事件会在 DrawOrder 属性中被调用,该属性用来确定精灵的绘制次序。
internal event EventHandler<SpiritEventArgs> Destroyed; internal event EventHandler<SpiritEventArgs> DrawOrderChanged; internal virtual void Destroy ( ) { if ( null != this.Destroyed ) this.Destroyed ( this, new SpiritEventArgs ( this ) ); } private int drawOrder = 0; internal int DrawOrder { get { return this.drawOrder; } set { this.drawOrder = value; if ( null != this.DrawOrderChanged ) this.DrawOrderChanged ( this, new SpiritEventArgs ( this ) ); } }字段 scene 用来表示控制精灵的场景,以后我们会将他的类型改为 IPlayScene。字段 world 表示控制场景的 World 类。字段 audioManager 用来控制音乐,我们将从场景中获取 AudioManager,而不是重新创建。
protected readonly IScene scene; private readonly World world; protected readonly AudioManager audioManager;字段 movie 表示精灵使用的电影,多个精灵可以共享同一个电影,字段 movieName 表示电影的名称。字段 extendMovie,extendMovieName 表示扩展的电影以及扩展的电影的名称。扩展电影可以用来播放特殊的效果,比如:玩家受伤后发出红色的光。
属性 Type 表示精灵的类型,你可以根据精灵的类型来确定精灵具体是什么东西。
protected readonly Movie movie; private readonly string movieName; protected readonly Movie extendMovie; private readonly string extendMovieName; protected int type; internal virtual int Type { get { return this.type; } set { this.type = value; } }字段 Width,Height 用来表示精灵的大小,而字段 halfSize 用来表示精灵尺寸的一半,我们会预先计算这个值,以避免重复计算。
internal readonly int Width; internal readonly int Height; protected readonly Vector2 halfSize;字段 Location 表示精灵的位置,属性 Angle 表示精灵的角度,如果字段 isRotable 为 false,则 Angle 是不能被修改的,字段 isMovieRotable 则表示电影的角度是否同时被修改。每当 Angle 被修改后,我们会调用 updateSpeed 方法来更新速度。
字段 speed 表示精灵的速度,字段 xSpeed,ySpeed 分别表示精灵在 x,y 轴上的速度。如果修改 Speed 属性,xSpeed 和 ySpeed 并不会被修改,但是你可以在派生类中修改 updateSpeed 方法来完成。
字段 protoSpeed,protoXSpeed,protoYSpeed 用来记录速度的原始值。字段 spiritBatch 用来绘制电影。
字段 isMovable 表示精灵是否可以移动,字段 isMoving 表示精灵是否正在移动中。
internal Vector2 Location; internal readonly HitArea HitArea; protected int angle; internal virtual int Angle { get { return this.angle; } set { if ( !this.isRotable ) return; value = Calculator.Degree ( value ); this.angle = value; if ( this.isMovieRotable ) { this.movie.Rotation = value; if ( null != this.extendMovie ) this.extendMovie.Rotation = value; } this.updateSpeed ( ); } } private float protoSpeed; private float protoXSpeed; private float protoYSpeed; protected float speed; protected float xSpeed; protected float ySpeed; public virtual float Speed { get { return this.speed; } set { this.speed = value; this.protoSpeed = this.speed; this.updateSpeed ( ); } } private SpriteBatch spiritBatch; protected bool isMoving = false; protected bool isMovable = true; protected bool isRotable = true; private readonly bool isMovieRotable;字段 destroyFrameCount 用来表示销毁精灵的帧数,字段 isAreaLimited 表示精灵是否可以超出 World 的 BattleArea,字段 isAreaEntered 表示精灵是否已经进入 BattleArea 中,字段 areaFrameCount 表示精灵进入 BattleArea 的限制时间。(但这里没有展示关于 isAreaLimited,isAreaEntered,areaFrameCount 的代码。)
字段 IsVisible 表示精灵是否可视。
private long destroyFrameCount; private readonly bool isAreaLimited; protected bool isAreaEntered; private long areaFrameCount; internal bool IsVisible = true;在 Spirit 的构造函数中,我们将初始化这些字段。
protected Spirit ( IScene scene, int type, Vector2 location, string movieName, string extendMovieName, float speed, int angle, HitArea hitArea, int width, int height, double destroySecond, bool isMovieRotable, bool isAreaLimited, bool isAreaEntered, double areaSecond ) { if ( null == scene || string.IsNullOrEmpty ( movieName ) ) throw new ArgumentNullException ( "scene, movieName", "scene, movieName can't be null" ); this.destroyFrameCount = World.ToFrameCount ( destroySecond ); this.scene = scene; this.world = scene.World; this.audioManager = scene.AudioManager; this.isMovieRotable = isMovieRotable; this.isAreaLimited = isAreaLimited; this.isAreaEntered = isAreaEntered; this.areaFrameCount = World.ToFrameCount ( areaSecond ); this.Location = location; this.movie = Movie.Clone ( this.scene.Makings[movieName] as Movie ); this.movie.Ended += this.movieEnded; this.movieName = movieName; if ( !string.IsNullOrEmpty ( extendMovieName ) ) { this.extendMovie = Movie.Clone ( this.scene.Makings[extendMovieName] as Movie ); this.extendMovieName = extendMovieName; } this.Width = width; this.Height = height; this.halfSize = new Vector2 ( width / 2, height / 2 ); this.Type = type; this.Speed = speed; this.Angle = angle; this.HitArea = hitArea; if ( null != this.HitArea ) this.HitArea.Locate ( this.getHitAreaLocation ( ) ); }在方法 LoadContent 中,我们将设置电影的相关内容,并从 World 中获取 SpriteBatch。在 Dispose 方法中我们销毁了一些对象。
internal virtual void LoadContent ( ) { this.spiritBatch = this.scene.World.Services.GetService ( typeof ( SpriteBatch ) ) as SpriteBatch; this.movie.Texture = ( this.scene.Makings[ this.movieName ] as Movie ).Texture; if ( null != this.extendMovie ) this.extendMovie.Texture = ( this.scene.Makings[ this.extendMovieName ] as Movie ).Texture; } public void Dispose ( ) { this.Dispose ( true ); } protected virtual void Dispose ( bool disposing ) { if ( disposing ) { this.movie.Ended -= this.movieEnded; this.movie.Dispose ( ); if ( null != this.extendMovie ) this.extendMovie.Dispose ( ); if ( null != this.HitArea ) this.HitArea.Dispose ( ); } }在 Update 方法中,我们将播放动画,并根据可用性调用 updating 方法。而在 updating 方法中,我们将判断是否需要销毁精灵,以及是否需要移动精灵,使碰撞区的位置和精灵的位置保持一致。
我们允许碰撞区和精灵的位置存在偏移,你可以通过 getHitAreaLocation 方法修改他。
internal void Update ( GameTime time ) { Movie.NextFrame ( this.movie ); if ( null != this.extendMovie ) Movie.NextFrame ( this.extendMovie ); if ( this.scene.IsEnabled && this.world.IsEnabled ) this.updating ( time ); } protected virtual void updating ( GameTime time ) { if ( this.destroyFrameCount > 0 && --this.destroyFrameCount <= 0 ) { this.Destroy ( ); return; } if ( this.isMoving && this.isMovable ) this.move ( ); if ( null != this.HitArea ) this.HitArea.Locate ( this.getHitAreaLocation ( ) ); } protected virtual Point getHitAreaLocation ( ) { return new Point ( ( int ) this.Location.X, ( int ) this.Location.Y ); }在 Draw 方法中,我们将根据一些条件来决定是否调用方法 drawing。而在 drawing 方法中,我们将调整电影的位置,并绘制他们。
同样,电影的位置可以不同于精灵的位置,你可以通过方法 getMovieLocation 来调整他。
internal void Draw ( GameTime time ) { if ( !this.scene.IsClosed && this.IsVisible ) { this.spiritBatch.Begin ( ); this.drawing ( time, this.spiritBatch ); this.spiritBatch.End ( ); } } protected virtual void drawing ( GameTime time, SpriteBatch batch ) { this.movie.Location = this.getMovieLocation ( ); Movie.Draw ( this.movie, time, batch ); if ( null != this.extendMovie ) { this.extendMovie.Location = this.movie.Location; Movie.Draw ( this.extendMovie, time, batch ); } } protected virtual Vector2 getMovieLocation ( ) { return this.Location; }方法 movieEnded 将在电影播放完毕之后调用,方法 Execute 用来执行一些命令,在本示例中不会用到。
方法 move 和 updateSpeed 是需要派生类修改的,以设置精灵的速度和移动。
protected virtual void movieEnded ( object sender, MovieEventArgs e ) { } internal virtual void Execute ( int action ) { } protected virtual void move ( ) { } protected virtual void updateSpeed ( ) { this.protoXSpeed = this.xSpeed; this.protoYSpeed = this.ySpeed; }精灵还有一些播放电影的方法,这里不再累述。
修改 World
我们需要为 World 增加一个管理精灵的类,代码如下:
internal sealed class SpiritCollection { private readonly List<Spirit> spirits = new List<Spirit> ( ); private bool isInitialized = false; internal SpiritCollection ( ) { } internal void Initialize ( ) { if ( this.isInitialized ) return; foreach ( Spirit spirit in this.spirits.ToArray ( ) ) spirit.LoadContent ( ); this.isInitialized = true; } internal void Update ( GameTime time ) { foreach ( Spirit spirit in this.spirits.ToArray ( ) ) spirit.Update ( time ); } internal void Draw ( GameTime time ) { foreach ( Spirit spirit in this.spirits.ToArray ( ) ) spirit.Draw ( time ); } internal void Add ( Spirit spirit ) { if ( spirit == null || this.spirits.Contains ( spirit ) ) return; if ( isInitialized ) spirit.LoadContent ( ); spirit.DrawOrderChanged += this.drawOrderChanged; this.spirits.Add ( spirit ); this.spirits.Sort ( DrawableSort ); } internal bool Remove ( Spirit spirit ) { if ( spirit == null ) return false; spirit.DrawOrderChanged -= this.drawOrderChanged; return this.spirits.Remove ( spirit ); } private void drawOrderChanged ( object sender, SpiritEventArgs e ) { this.spirits.Sort ( DrawableSort ); } private static int DrawableSort ( Spirit a, Spirit b ) { return a.DrawOrder.CompareTo ( b.DrawOrder ); } }然后我们为 World 增加一个名称为 Components 的字段,用来管理精灵。
internal readonly SpiritCollection Components = new SpiritCollection ( );
示例
在 SceneT14 场景中,我们创建了一个精灵 bird,另外,我们有两个按钮,点击 Play,小鸟将移动,点击 Stop,小鸟停止移动。
下面是小鸟的代码,在代码中,我们通过修改 updateSpeed,move 方法实现了小鸟的移动。并通过 Go 和 Stop 方法控制了小鸟的移动。
internal class Bird : Spirit { internal Bird ( IScene scene, Vector2 location ) : base ( scene, 0, location, "bird", null, 4, 0, new SingleRectangleHitArea ( new Rectangle ( -40, -40, 80, 80 ) ), 80, 80, 0, true, false, false, 0 ) { } protected override void updateSpeed ( ) { this.xSpeed = this.speed; this.ySpeed = this.speed; base.updateSpeed ( ); } protected override void move ( ) { this.Location.X += this.xSpeed; this.Location.Y += this.ySpeed; } internal void Go ( ) { this.isMoving = true; this.PlayMovie ( "go" ); } internal void Stop ( ) { this.isMoving = false; this.PlayMovie ( "stop" ); } }在两个按钮的 Selected 事件中,我们分别让小鸟移动和停止,当然,我们忘记了注销事件。
internal sealed class SceneT14 : CommandScene { // ... private Bird bird; private readonly Button goButton; private readonly Button stopButton; internal SceneT14 ( ) : base ( Vector2.Zero, GestureType.None, "background1", new Resource[] { new Resource ( "bird2.image", ResourceType.Image, @"image\bird2" ), new Resource ( "go.image", ResourceType.Image, @"image\button1" ), new Resource ( "stop.image", ResourceType.Image, @"image\button2" ), }, new Making[] { new Movie ( "bird", "bird2.image", 80, 80, 5, "stop", new MovieSequence ( "go", true, new Point ( 1, 1 ), new Point ( 2, 1 ) ), new MovieSequence ( "stop", true, new Point ( 3, 1 ) ) ), new Button ( "b.go", "go.image", "GO", new Vector2 ( 100, 100 ), 100, 50, new Point ( 1, 1 ) ), new Button ( "b.play", "stop.image", "STOP", new Vector2 ( 100, 300 ), 100, 50, new Point ( 1, 1 ) ) } ) { this.goButton = this.makings[ "b.go" ] as Button; this.stopButton = this.makings[ "b.play" ] as Button; this.goButton.Selected += this.goButtonSelected; this.stopButton.Selected += this.stopButtonSelected; } private void goButtonSelected ( object sender, ButtonEventArgs e ) { this.bird.Go ( ); } private void stopButtonSelected ( object sender, ButtonEventArgs e ) { this.bird.Stop ( ); } public override void LoadContent ( ) { base.LoadContent ( ); this.bird = new Bird ( this, new Vector2 ( 200, 100 ) ); this.bird.LoadContent ( ); this.world.Components.Add ( this.bird ); } public override void UnloadContent ( ) { this.world.Components.Remove ( this.bird ); this.bird.Dispose ( ); base.UnloadContent ( ); } }
本期视频 http://v.youku.com/v_show/id_XNTgwOTE3NTky.html
项目地址 http://wp-xna.googlecode.com/
更多内容 WPXNA
平方开发的游戏 http://zoyobar.lofter.com/
QQ 群 213685539
欢迎访问我在其他位置发布的同一文章:http://www.wpgame.info/post/decc4_74291e