二十節不間斷更新,暫作小結整理思緒,為第二部分更精彩內容鋪路:跟隨式地圖移動、NPC/怪物互動、AI、攻擊魔法、傷害計算、完美 RPG 界面等。
第十九節雖實現全方向動作,但每次移動皆啟動 A*,造成資源浪費且非完美。改進型完美 A* 模式:移動時先建立兩點直線移動,即時檢測障礙物;無碰撞則保持直線至終點;一旦碰到障礙物,即時啟動 A* 繞行至目的地。
傳統座標還原法會造成畫面彈跳,啟發式預測法即時預測前方單元格:
// 判斷是否將要碰撞障礙物(障礙物預測法)
private bool WillCollide() {
switch ((int)Spirit.Direction) {
case 0: return Matrix[(int)(Spirit.X / GridSize), (int)(Spirit.Y / GridSize) - 1] == 0;
case 1: return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize) - 1] == 0;
case 2: return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize)] == 0;
case 3: return Matrix[(int)(Spirit.X / GridSize) + 1, (int)(Spirit.Y / GridSize) + 1] == 0;
case 4: return Matrix[(int)(Spirit.X / GridSize), (int)(Spirit.Y / GridSize) + 1] == 0;
case 5: return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize) + 1] == 0;
case 6: return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize)] == 0;
case 7: return Matrix[(int)(Spirit.X / GridSize) - 1, (int)(Spirit.Y / GridSize) - 1] == 0;
default: return true;
}
}
直線移動方法 NormalMoveTo() 新增朝向動畫,計算 totalcost 確保任意角度勻速移動:
private void NormalMoveTo(Point p) {
int totalcost = (int)Math.Sqrt(Math.Pow(p.X - Spirit.X, 2) + Math.Pow(p.Y - Spirit.Y, 2)) / GridSize * UnitMoveCost;
...
double direction = Super.GetDirectionByTan(p.X, p.Y, Spirit.X, Spirit.Y);
var doubleAnimation = new DoubleAnimation(direction, direction, new Duration(TimeSpan.FromMilliseconds(totalcost)));
Storyboard.SetTarget(doubleAnimation, Spirit);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Direction"));
storyboard.Children.Add(doubleAnimation);
storyboard.Begin();
}
滑鼠點擊啟動直線移動,設定 IsAStarMoving = false:
private void Carrier_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Point p = e.GetPosition(Map);
if (Matrix[(int)p.X / GridSize, (int)p.Y / GridSize] != 0) {
Spirit.Destination = p; Spirit.Action = Actions.Run;
Spirit.IsAStarMoving = false; NormalMoveTo(p);
}
}
視窗刷新線程中銜接邏輯:
private void Timer_Tick(object sender, EventArgs e) {
if (ArriveTarget()) Spirit.Action = Actions.Stop;
else if (!Spirit.IsAStarMoving && WillCollide()) {
AStarMoveTo(Spirit.Destination);
Spirit.IsAStarMoving = true;
}
}
IsAStarMoving 為直線與 A* 移動的樞紐,避免重複判斷。測試成果:
第十一節遮罩層有瑕疵。新增標誌物 Mask3.png,設定 Canvas.SetZIndex(Mask, 612)(高度 + Y 值),精靈 ZIndex = Y 即時更新,實現畫家算法完美遮擋:
UI 添加下拉選單與按鈕,三行代碼實現:
private void ChangeEquipment(object sender, RoutedEventArgs e) {
Spirit.Equipment[0] = Convert.ToInt32(comboBox1.SelectionBoxItem.ToString());
Spirit.Equipment[1] = Convert.ToInt32(comboBox2.SelectionBoxItem.ToString());
Spirit.Source = Super.EquipPart(Spirit.Equipment, Spirit.DirectionNum, Spirit.DirectionFrameNum,
Spirit.TotalWidth, Spirit.TotalHeight, Spirit.SingleWidth, Spirit.SingleHeight);
}
首次換裝因合成計算會卡頓,再次換裝因快取極快,符合網遊體驗:
針對讀者反饋 A* 難度高,補充核心概念 GridSize(單位格尺寸):
GridSize=20 時 10000×10000 地圖只需 Matrix[500,500]GridSize=20 僅 4 個節點,GridSize=1 需 1600 個節點
本節結束,第二部分開始,將深入真正 2D 遊戲製作核心領域。