2D 動畫與碰撞

為了讓遊戲更生動,幾乎所有遊戲都會出現 2D 動畫,2D 遊戲就不用說了,3D 遊戲裡的選單和畫面狀態等也是 2D 動畫常出現的地方。

2D 動畫簡單的想法就是在短時間內變化各種不同的圖片,讓人感覺是會動的,通常我們會將相關的動作畫在同一張圖上,然後固定時間轉換顯示的位置,如下圖

先顯示最左邊的範圍,接著下一步再顯示第二個動作的範圍,依此類推,就會像動畫一樣了。整張的原圖如下:

載入整張圖片後,再藉由改變 SourceRectangle 這個參數來決定要畫哪一個部分,而為了讓動畫設定較有彈性,可以設計一些簡單的資料結構。首先是 FrameData:

public class FrameData {
    public Rectangle FrameRect { get; set; }
    public float Time { get; set; }
}

由他決定要取得的矩形範圍和停留時間。再來是 AnimationData:

public class AnimationData {
    private Texture2D _Texture;
    public Texture2D Texture {
        get { return _Texture; }
    }

    private bool _IsLoop;
    public bool IsLoop {
        get { return _IsLoop; }
    }

    private List<FrameData> _Frames;
    public List<FrameData> Frames {
        get { return _Frames; }
    }

    public AnimationData(Texture2D texture, bool isLoop, List<FrameData> frames) {
        _Frames = frames;
        _Texture = texture;
        _IsLoop = isLoop;
    }
}

一個完整的動畫會包含多個 Frame,也在此設定是否需要循環撥放。這些都是播放動畫時的一些基本資訊,再來增加一個 AnimationPlayer 物件,依據所給的資料來畫圖,程式碼如下:

public class AnimationPlayer {
    private int FrameIndex;
    private float Time;
    private AnimationData Data;

    public Vector2 Position;
    public Vector2 Origin;
    public SpriteEffects Effect;

    public AnimationPlayer(AnimationData data) {
        Data = data;
        FrameIndex = 0;
        if (Data.Frames.Count <= 0) throw new Exception("動畫無資料!");
        Time = Data.Frames[0].Time;            
    }

    public void Draw(float time, SpriteBatch sprite) {
        sprite.Draw(Data.Texture, Position, Data.Frames[FrameIndex].FrameRect, Color.White, 0, Origin, 1, Effect, 0);

        Time -= time;
        if (Time <= 0) {
            NextFrame();
            Time = Data.Frames[FrameIndex].Time;
        }
    }

    private void NextFrame() {
        if (FrameIndex >= Data.Frames.Count - 1) {
            if (Data.IsLoop == true) {
                FrameIndex = 0;
            }
        } else FrameIndex++;
    }
}

他的工作重點就是讓每一張圖顯示固定時間後再切換到下一張。有了這些功能後,在 LoadContent 時載入圖片和資料

protected override void LoadContent() {
    spriteBatch = new SpriteBatch(GraphicsDevice);

    Texture2D animationTexture = Content.Load<Texture2D>("People");
    List<FrameData> list = new List<FrameData>{ 
        new FrameData{ FrameRect = new Rectangle(0,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(96,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(192,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(288,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(384,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(480,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(576,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(672,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(768,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(864,0,96,96), Time = 200}};
    AnimationData data = new AnimationData(animationTexture, true, list);
    AnimationPlayer = new AnimationPlayer(data);
    AnimationPlayer.Position = new Vector2(100, 100);

}

上面程式碼的 list 就是設定每一個 frame 的位置和顯示時間,再將圖片和 frame list 給 AnimationData。 然後是 Draw 函式

protected override void Draw(GameTime gameTime) {
    GraphicsDevice.Clear(Color.CornflowerBlue);

    float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

    // TODO: Add your drawing code here
    spriteBatch.Begin();
    AnimationPlayer.Draw(time, spriteBatch);
    spriteBatch.End();
    base.Draw(gameTime);

如此一來就可以在畫面上顯示跑步的動作了,藉由設定每張 frame 停留的時間,可以改變動畫的快慢。

再來設計一個腳色可以運用此動畫來移動,配合之前教的移動概念來做,腳色會有位置與速度的屬性,並且加上 AnimationPlayer 物件用來顯示動畫,以及 SpriteEffects 屬性來決定往左或往又跑。腳色物件的程式碼如下:

public class Role {
    private AnimationPlayer Animation;
    private Vector2 Origin;
    private SpriteEffects Direction;

    private Vector2 _Position;
    public Vector2 Position {
        get { return _Position; }
        set { _Position = value; }
    }

    private Vector2 _Velocity;
    public Vector2 Velocity {
        get { return _Velocity; }
        set {
            _Velocity = value;
            if (_Velocity.X < 0) Direction = SpriteEffects.None;
            else Direction = SpriteEffects.FlipHorizontally;
        }
    }

    public Role(AnimationData data) {
        Direction = SpriteEffects.None;
        Origin = new Vector2(48, 48);
        Animation = new AnimationPlayer(data);
        Animation.Origin = Origin;
    }

    public void Update(float time) {
        _Position += _Velocity * time;
        if (_Position.X + Origin.X > 800) {
            _Position.X = 800 - Origin.X;
            Velocity = Vector2.Multiply(Velocity, -Vector2.UnitX);
        } else if (_Position.X - Origin.X < 0) {
            _Position.X = Origin.X;
            Velocity = Vector2.Multiply(Velocity, -Vector2.UnitX);
        }
    }

    public void Draw(float time, SpriteBatch sprite) {
        Animation.Position = _Position;
        Animation.Effect = Direction;
        Animation.Draw(time, sprite);
    }
}

在 Velocity 屬性改變時,決定面向左邊還是右邊,這裡用到的 SpriteEffects 列舉讓圖片左右鏡射,如此就不必準備兩個方向的圖了。而 Update 裡面判斷是不是超過螢幕,超過的話就轉換方向,而 Draw 的話就是把相關資訊設定給 AnimationPlayer 然後讓他畫出圖來。最後是在 game.cs 裡使用 Role 物件,相關程式碼如下:

Role Player1;

protected override void LoadContent() {
    spriteBatch = new SpriteBatch(GraphicsDevice);
    Texture2D animationTexture = Content.Load<Texture2D>("People");
    List<FrameData> list = new List<FrameData>{ 
        new FrameData{ FrameRect = new Rectangle(0,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(96,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(192,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(288,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(384,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(480,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(576,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(672,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(768,0,96,96), Time = 200},
        new FrameData{ FrameRect = new Rectangle(864,0,96,96), Time = 200}};
    AnimationData data = new AnimationData(animationTexture, true, list);
 
    Player1 = new Role(data);
    Player1.Position = new Vector2(100, 100);
    Player1.Velocity = new Vector2(0.1f, 0);
}
protected override void Update(GameTime gameTime) {
    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();

    float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds; 
    Player1.Update(time);

    base.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
    GraphicsDevice.Clear(Color.CornflowerBlue);

    float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

    // TODO: Add your drawing code here
    spriteBatch.Begin();
    Player1.Draw(time, spriteBatch);
    spriteBatch.End();
    base.Draw(gameTime);
}

由於傳入 Update 和 Draw 的時間我統一用毫秒,所以 Role 裡面的速率是像素每毫秒,這點設定時要稍微注意。而除了建構 Role 物件時必要的設定,Update 和 Draw 都只是分別呼叫相關函式即可。下面是執行畫面。他會慢慢跑。

接著來談碰撞,碰撞在大部分遊戲裡都扮演很重要的腳色,而最簡單的碰撞偵測就是直接測試矩形有沒有重疊,讓我們實作看看。

先在 Role 類別加上 

private Rectangle _Bounding;
public Rectangle Bounding {
    get { return _Bounding; }
}

這是用來設定腳色會發生碰撞的範圍。然後在 Update 中不斷更新此數據

_Bounding = new Rectangle((int)(_Position.X - Origin.X),(int)( _Position.Y - Origin.Y), 96, 96);

然後在 game.cs 中再增加一個 Player2 讓他和 Player1 一起移動,在 Update 裡檢查兩個腳色是否有碰撞,有的話就彼此交換速度。

if (Player1.Bounding.Intersects(Player2.Bounding) == true) {
    Vector2 tempV = Player1.Velocity;
    Player1.Velocity = Player2.Velocity;
    Player2.Velocity = tempV;
}

關鍵就是用 Rectangle.Intersects 來檢查兩個矩形有沒有發生重疊。以下是程式執行畫面,兩個腳色發生碰撞後會互相交換速度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值