XNA 世界地圖/座標系轉換

  • 4825
  • 0
  • 2011-11-19

當遊戲的世界地圖很大超過螢幕時,我們就要使用世界地圖座標來撰寫遊戲邏輯,並在Draw()要轉換為螢幕座標來繪製精靈。

當遊戲的地圖是在螢幕範圍內,那麼精靈的位置是以螢幕左上角為原點的座標系,在繪製Draw()裡也是以螢幕左上角為原點的座標系,但是如果遊戲的地圖是比螢幕還大,那麼精靈的位置就要以世界地圖為座標系,繪製Draw()裡是以螢幕為座標系,遊戲邏輯、精靈位置變動及碰撞偵測是以世界地圖為座標系,所以這之間就存在了世界地圖與螢幕座標系的轉換,下圖就表示之間的關係。

 

image

 

現在我們要做的是創建一個Camera類別來定義上圖的關係,並設定為NotInheritable類別,不需要產生instance就可以直接使用,Camera類別的屬性有Position:表示Camera在世界的座標,這裡用了輔助類別MathHelper.Clam()來限制Camera的ViewPort不可超出世界地圖。WorldRectangle:世界地圖的矩形邊界,ViewPort:ViewPort的矩形邊界,其值為Camera的位置與ViewPort大小。ViewPortWidth、ViewPortHeight:ViewPort的寬高。Camera的方法有Move():傳入一個速度,改變Camera的位置。IsObjVisible()傳入精靈的矩形,傳回精靈是否在ViewPort內。Transform():把世界座標轉成螢幕座標。

Public NotInheritable Class Camera
    Private Shared mPosition As Vector2 = Vector2.Zero
    Private Shared mWorldRectangle As Rectangle = New Rectangle(0, 0, 0, 0)
    Private Shared mViewPortSize As Vector2 = Vector2.Zero
 
    Public Shared Property Position As Vector2  'Camera的世界座標位置,不可讓ViewPort超出世界
        Get
            Return mPosition
        End Get
        Set(ByVal value As Vector2)
            mPosition = New Vector2(MathHelper.Clamp(value.X, mWorldRectangle.X, mWorldRectangle.Width - mViewPortSize.X), MathHelper.Clamp(value.Y, mWorldRectangle.Y, mWorldRectangle.Height - mViewPortSize.Y))
        End Set
    End Property
 
    Public Shared Property WorldRetangle As Rectangle '世界的矩形
        Get
            Return mWorldRectangle
        End Get
        Set(ByVal value As Rectangle)
            mWorldRectangle = value
        End Set
    End Property
 
    Public Shared ReadOnly Property ViewPort As Rectangle 'ViewPort的矩形
        Get
            Return New Rectangle(CInt(mPosition.X), CInt(mPosition.Y), CInt(mViewPortSize.X), CInt(mViewPortSize.Y))
        End Get
    End Property
 
    Public Shared Property ViewPortWidth As Integer 'ViewPort的寬
        Get
            Return CInt(mViewPortSize.X)
        End Get
        Set(ByVal value As Integer)
            mViewPortSize.X = value
        End Set
    End Property
 
    Public Shared Property ViewPortHeight As Integer 'ViewPort的高
        Get
            Return CInt(mViewPortSize.Y)
        End Get
        Set(ByVal value As Integer)
            mViewPortSize.Y = value
        End Set
    End Property
 
    Public Shared Sub Move(ByVal v As Vector2) 'Camera移動 (Method)
        Position += v
    End Sub
 
    Public Shared Function IsObjVisible(ByVal bounds As Rectangle) As Boolean 'Check精靈是不是在ViewPort裡
        Return ViewPort.Intersects(bounds)
    End Function
 
    Public Shared Function Transform(ByVal p As Vector2) As Vector2 '轉換世界座標到螢幕座標
        Return p - Position
    End Function
 
    Public Shared Function Transform(ByVal t As Rectangle) As Rectangle '轉換世界矩形到螢幕矩形
        Return New Rectangle(t.Left - CInt(Position.X), t.Top - CInt(Position.Y), t.Width, t.Height)
    End Function
End Class

 

這邊我再創建一個精靈類別繼承可繪製遊戲組件DrawableGameComponent,屬性包含世界座標位置、螢幕座標位置、速度、世界座標矩形、螢幕座標矩形,在Update()有移動程式碼,Draw()如果精靈是出現在Camera的ViewPort內則繪製精靈。

Public Class Sprite
    Inherits Microsoft.Xna.Framework.DrawableGameComponent
 
    Dim spriteBatch As SpriteBatch
 
    Public Texture As Texture2D
    Public Velocity As Vector2 = Vector2.Zero
    Public WorldLocation As Vector2 = Vector2.Zero
 
    Public ReadOnly Property ScreenLocation As Vector2 '螢幕座標系位置
        Get
            Return Camera.Transform(WorldLocation)
        End Get
    End Property
 
    Public ReadOnly Property RectWorld As Rectangle '世界座標矩形
        Get
            Return New Rectangle(CInt(WorldLocation.X), CInt(WorldLocation.Y), Texture.Width, Texture.Height)
        End Get
    End Property
 
    Public ReadOnly Property RectScreen As Rectangle '螢幕座標矩形
        Get
            Return Camera.Transform(RectWorld)
        End Get
    End Property
 
    Public Sub New(ByVal game As Game, _worldLocation As Vector2, _texture As Texture2D) '建構子,傳入世界地圖座標位置與紋理
        MyBase.New(game)
        ' TODO: 在此建構任何子元件
        WorldLocation = _worldLocation
        Texture = _texture
    End Sub
 
    ''' <summary>
    ''' 允許遊戲元件在開始執行前執行所需的任何初始化項目。
    ''' 這是元件能夠查詢所需服務,以及載入內容的地方。
    ''' </summary>
    Public Overrides Sub Initialize()
        ' TODO: 在此新增初始化程式碼
        MyBase.Initialize()
    End Sub
 
    Protected Overrides Sub LoadContent()
        spriteBatch = New SpriteBatch(GraphicsDevice)
 
        MyBase.LoadContent()
    End Sub
 
    ''' <summary>
    ''' 允許遊戲元件自我更新。
    ''' </summary>
    ''' <param name="gameTime">提供時間值的快照。</param>
    Public Overrides Sub Update(ByVal gameTime As GameTime)
        ' TODO: 在此新增更新程式碼 
        WorldLocation += Velocity '移動
 
        MyBase.Update(gameTime)
    End Sub
 
    Public Overrides Sub Draw(ByVal gameTime As GameTime)
        If Camera.IsObjVisible(RectWorld) Then
            spriteBatch.Begin()
            spriteBatch.Draw(Texture, ScreenLocation, Color.White)
            spriteBatch.End()
        End If
 
        MyBase.Draw(gameTime)
    End Sub
End Class

.
建置完以上兩個類別後,我們開始寫一些測試程式,主程式包含兩個精靈,一個世界地圖,Camera從左邊往右水平移動,精靈1往左移動。
image
 

在Game1裡宣告變數

Public Class Game1
    Inherits Microsoft.Xna.Framework.Game
 
    Private WithEvents graphics As GraphicsDeviceManager
    Private WithEvents spriteBatch As SpriteBatch
 
    Dim map As Sprite
    Dim sprite1 As Sprite
    Dim sprite2 As Sprite
End Class
 
在LoadContent(),New了map、sprite1、sprite2三個實例,並設定它們的世界座標位置及圖片紋理,再把它們加入遊戲元件,這樣它們的Update()跟Draw()就會同步被呼叫,最後設定Camera的世界地圖矩形與ViewPort大小。
Protected Overrides Sub LoadContent()
        ' 建立可用來繪製紋理的新 SpriteBatch。
        spriteBatch = New SpriteBatch(GraphicsDevice)
 
        ' TODO: 在此使用 Me.Content 載入您的遊戲內容
        map = New Sprite(Me, New Vector2(0, 0), Content.Load(Of Texture2D)("TestMap"))
        sprite1 = New Sprite(Me, New Vector2(950, 50), Content.Load(Of Texture2D)("red"))
        sprite2 = New Sprite(Me, New Vector2(950, 50), Content.Load(Of Texture2D)("red"))
 
        sprite1.DrawOrder = 1 '繪製順序
        sprite1.Initialize()
        Components.Add(sprite1)
 
        sprite2.DrawOrder = 1 '繪製順序
        sprite2.Initialize()
        Components.Add(sprite2)
 
        map.DrawOrder = 0 '繪製順序
        map.Initialize()
        Components.Add(map)
 
        Camera.WorldRetangle = New Rectangle(0, 0, map.RectWorld.Width, map.RectWorld.Height)
        Camera.ViewPortWidth = 800
        Camera.ViewPortHeight = 480
 End Sub

 

在Update()撰寫Camera及sprite1移動,這邊有沒有發現兩個是可以獨立移動的。

Protected Overrides Sub Update(ByVal gameTime As GameTime)
        ' 允許遊戲結束
        If GamePad.GetState(PlayerIndex.One).Buttons.Back = ButtonState.Pressed Then
            Me.Exit()
        End If
 
        ' TODO: 在此新增您的更新邏輯
        sprite1.Velocity = New Vector2(-5, 0)
        Camera.Move(New Vector2(5, 0))
        MyBase.Update(gameTime)
End Sub

 

Draw()不需要寫,因為sprite類別物件會自己繪製,程式執行結果如下 (範例下載) (下載位置2)

大家有沒有發現移動Camera就是捲動效果。