在上一節(十八)的 ChangeAction() 方法中,我們提到它會根據精靈當前的動作來設定切圖動畫的起始幀與結束幀。這兩個參數 CurrentStartFrame 與 CurrentEndFrame 至關重要,因為在精靈的生命線程中,我們透過以下核心代碼實現連續動畫:
// 精靈線程間隔事件
private void Timer_Tick(object sender, EventArgs e) {
...
// 動態更改精靈圖片源以形成連續動作
Body.Source = Source[(int)Direction, FrameCounter];
FrameCounter = FrameCounter == CurrentEndFrame ? CurrentStartFrame : FrameCounter + 1;
}
以「跑步」動作為例:FrameCounter 從 5 開始計數,以 1 為單位遞增,目標為 12;當到達 12 時回到 5 重複。同理,「施法」動作從 20 計數至 25 後回到 20。其他動作以此類推。
第六節末尾曾略提相關實現方法,但未深入。本節將完整實現八方向精靈的正確朝向顯示與精確定位停止。實現步驟分析如下:
Spirit.X、Spirit.Y)下圖右半部分註釋已詳細說明原理,以下為通用判斷朝向方法:
/// <summary>
/// 透過正切值獲取精靈朝向代號
/// </summary>
/// <param name="targetX">目標點 X</param>
/// <param name="targetY">目標點 Y</param>
/// <param name="currentX">當前點 X</param>
/// <param name="currentY">當前點 Y</param>
/// <returns>朝向代號(以北為 0,順時針 1-7)</returns>
public static double GetDirectionByTan(double targetX, double targetY, double currentX, double currentY) {
double tan = (targetY - currentY) / (targetX - currentX);
if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && targetY <= currentY) return 0;
else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX > currentX && targetY < currentY) return 1;
else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && targetX >= currentX) return 2;
else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX > currentX && targetY > currentY) return 3;
else if (Math.Abs(tan) >= Math.Tan(Math.PI * 3 / 8) && targetY >= currentY) return 4;
else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX < currentX && targetY > currentY) return 5;
else if (Math.Abs(tan) <= Math.Tan(Math.PI / 8) && targetX <= currentX) return 6;
else if (Math.Abs(tan) > Math.Tan(Math.PI / 8) && Math.Abs(tan) < Math.Tan(Math.PI * 3 / 8) && targetX < currentX && targetY < currentY) return 7;
else return 0;
}
註:Math.Tan() 參數為弧度單位,角度需透過公式轉換。演算法邏輯是否最優見仁見智,歡迎提供更優解法。
在滑鼠左鍵點擊事件中獲取目標點,切換主角為跑步狀態並啟動 A* 尋路:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Map); // 點擊處在 Map 中的座標
Spirit.Action = Actions.Run; // 切換為跑步狀態
AStarMoveTo(p); // 開始尋路
}
上兩節的 AStarMoveTo() 僅建立 X、Y 序列幀動畫。為實現角色時時朝向,需額外建立 Direction 序列幀動畫,改進後的方法如下:
private void AStarMoveTo(Point p) {
...
// 建立 X 軸逐幀動畫
DoubleAnimationUsingKeyFrames keyFramesAnimationX = new DoubleAnimationUsingKeyFrames();
keyFramesAnimationX.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationX, Spirit);
Storyboard.SetTargetProperty(keyFramesAnimationX, new PropertyPath("X"));
// 建立 Y 軸逐幀動畫
DoubleAnimationUsingKeyFrames keyFramesAnimationY = new DoubleAnimationUsingKeyFrames();
keyFramesAnimationY.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationY, Spirit);
Storyboard.SetTargetProperty(keyFramesAnimationY, new PropertyPath("Y"));
// 建立朝向動畫(新增部分)
DoubleAnimationUsingKeyFrames keyFramesAnimationDirection = new DoubleAnimationUsingKeyFrames();
keyFramesAnimationDirection.Duration = new Duration(TimeSpan.FromMilliseconds(path.Count * cost));
Storyboard.SetTarget(keyFramesAnimationDirection, Spirit);
Storyboard.SetTargetProperty(keyFramesAnimationDirection, new PropertyPath("Direction"));
for (int i = 0; i < framePosition.Count(); i++) {
// X 軸關鍵幀
LinearDoubleKeyFrame keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == 0 ? Spirit.X : framePosition[i].X * GridSize;
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationX.KeyFrames.Add(keyFrame);
// Y 軸關鍵幀
keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == 0 ? Spirit.Y : framePosition[i].Y * GridSize;
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationY.KeyFrames.Add(keyFrame);
// 朝向關鍵幀(核心邏輯:後一點與前一點計算正切值決定方向)
keyFrame = new LinearDoubleKeyFrame();
keyFrame.Value = i == framePosition.GetUpperBound(0)
? Super.GetDirectionByTan(framePosition[i].X, framePosition[i].Y, framePosition[i - 1].X, framePosition[i - 1].Y)
: Super.GetDirectionByTan(framePosition[i + 1].X, framePosition[i + 1].Y, framePosition[i].X, framePosition[i].Y);
keyFrame.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(cost * i));
keyFramesAnimationDirection.KeyFrames.Add(keyFrame);
}
storyboard.Children.Add(keyFramesAnimationX);
storyboard.Children.Add(keyFramesAnimationY);
storyboard.Children.Add(keyFramesAnimationDirection);
...
}
方向動畫建立與 X、Y 動畫如出一轍,原理是將尋路點序列中後一點與前一點計算正切值確定方向,再將方向序列值作為關鍵幀加入 Storyboard。
測試發現角色抵達目的地後仍保持跑步狀態。新增判斷方法:
// 判斷是否移動到目的地
private bool ArriveTarget() {
return (storyboard != null && storyboard.GetCurrentProgress() == 1) ? true : false;
}
在遊戲視窗刷新主線程中調用:
// 遊戲視窗刷新主線程間隔事件
private void Timer_Tick(object sender, EventArgs e) {
// 判斷主角是否移動到目標,若是則切換為待機
if (ArriveTarget()) {
Spirit.Action = Actions.Stop;
}
...
}
歷經三節完成完美精靈系統,角色現可顯示正確方向並精確停止。下一節將對前 19 節內容進行補遺與拓展,作為教程第一大部分(1-20 節)的小結。