上一章建立精靈控件的 X、Y 依賴屬性,並在 UI 執行緒中以:
Canvas.SetLeft(Spirit, Spirit.X + Canvas.GetLeft(Map));
Canvas.SetTop(Spirit, Spirit.Y + Canvas.GetTop(Map));
實現精靈跟隨地圖移動。其中 Map 的 Canvas.Left/Top 隨滑鼠拖曳變化,Spirit.X/Y 代表角色在地圖局部座標系中的位置。要讓角色在地圖上「走路」,只需更新 Spirit.X/Y,UI 執行緒即自動同步顯示位置。
早期章節地圖不動,直接對 Canvas.Left/Top 建立 Storyboard 動畫,屬性路徑為 PropertyPath("(Canvas.Left)")。
改為對精靈控件的 X、Y 依賴屬性建立動畫:
Storyboard.SetTargetProperty(keyFramesAnimationX, new PropertyPath("X"));
Storyboard.SetTargetProperty(keyFramesAnimationY, new PropertyPath("Y"));
動畫不再直接操控視覺位置,而是產生「角色在地圖座標系中的軌跡序列」,UI 執行緒負責疊加地圖偏移量渲染。
int GridSize = 20; // 邏輯格子大小
int SpiritCenterX = 4; // 角色腳底相對圖片左側格數
int SpiritCenterY = 5; // 角色腳底相對圖片頂部格數
// 遊戲邏輯座標系(格子單位)
int GameSystemX => (int)(Spirit.X / GridSize) + SpiritCenterX;
int GameSystemY => (int)(Spirit.Y / GridSize) + SpiritCenterY;
Spirit.X/Y 以像素為單位,除以 GridSize 換算為格子座標,再加上腳底偏移量,得到 A* 演算法所需的網格節點索引。
private void AStarMoveTo(Point p)
{
// 起點:角色當前遊戲座標
int start_x = GameSystemX;
int start_y = GameSystemY;
Start = new System.Drawing.Point(start_x, start_y);
// 終點:滑鼠點擊處(相對地圖圖片座標)換算為格子座標
int end_x = (int)p.X / GridSize;
int end_y = (int)p.Y / GridSize;
End = new System.Drawing.Point(end_x, end_y);
if (path == null) return; // 路徑不存在
// ... A* 尋路取得 path (List<Point>) ...
// 建立 X 軸關鍵幀動畫
var keyFramesX = new DoubleAnimationUsingKeyFrames();
keyFramesX.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesX, Spirit);
Storyboard.SetTargetProperty(keyFramesX, new PropertyPath("X"));
// 建立 Y 軸關鍵幀動畫
var keyFramesY = new DoubleAnimationUsingKeyFrames();
keyFramesY.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesY, Spirit);
Storyboard.SetTargetProperty(keyFramesY, new PropertyPath("Y"));
for (int i = 0; i < framePosition.Count; i++)
{
var kfX = new LinearDoubleKeyFrame();
// 平滑銜接:第 0 幀用當前 Spirit.X,避免跳幀
kfX.Value = i == 0 ? Spirit.X : (framePosition[i].X - SpiritCenterX) * GridSize;
kfX.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesX.KeyFrames.Add(kfX);
var kfY = new LinearDoubleKeyFrame();
kfY.Value = i == 0 ? Spirit.Y : (framePosition[i].Y - SpiritCenterY) * GridSize;
kfY.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesY.KeyFrames.Add(kfY);
}
storyboard.Children.Add(keyFramesX);
storyboard.Children.Add(keyFramesY);
storyboard.Begin();
}
i == 0 ? Spirit.X : ... 確保動畫起始幀精確對齊角色當前像素位置,而非網格中心。若省略此判斷,角色在移動中途被重新指派新目標時,會瞬間「傳送」到最近的網格中心,造成視覺跳動。此技巧同樣適用於 Y 軸。
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point p = e.GetPosition(Map); // 取得點擊處在 Map 圖片上的相對座標
AStarMoveTo(p);
}
或使用 TranslatePoint 從 Carrier 座標轉換至 Map 座標,效果相同。TranslatePoint 在碰撞檢測、射程計算等幾何運算中更具優勢。
此時已具備「大地圖自由拖曳」+「角色自主寻路移動」的雙重機制,為後續回合制戰棋、即時戰略等玩法奠定基礎。
下一章將解決長期懸而未決的大問題:精靈八方向動作切換。將結合第 17 章的圖集素材,實現根據移動向量自動切換 8 方向待機/跑步/攻擊等動畫,真正實現「八面玲瓏」的完美精靈。