【演算法】遊戲中的學習,使用c#物件導向特性控制遊戲角色移動

lanedm發表於2023-10-12

最近,小悅的生活像是一首繁忙的交響曲,每天忙得團團轉,雖然她的日程安排得滿滿當當,但她並未感到充實。相反,她很少有時間陪伴家人,這讓她感到有些遺憾。在週五的午後,小悅的哥哥突然打來電話,他的聲音裡充滿了焦慮。

“小悅,我有個事情想拜託你。”哥哥的聲音傳來。

小悅不禁有些疑惑,哥哥有什麼事情需要她幫忙呢?她忍不住問:“哥哥,有什麼需要我幫忙的嗎?”

哥哥解釋說:“我最近要出差一段時間,大概一個星期左右。而我的妻子目前正在照顧住院的父母,沒有時間照顧小明。我想請你幫忙照顧小明一段時間。”

小悅愣住了,沒想到哥哥會把這個重任託付給她。這是一個不小的挑戰,她有些擔心自己無法勝任。但是,她知道她不能讓自己的感情影響到哥哥的決定。於是她堅定地回答:“好的,哥哥,我會照顧好小明的。你放心去出差吧。”

電話那頭的哥哥鬆了一口氣,感激地說:“謝謝你,小悅。我相信你會照顧好小明的。我會盡快回來的。”

就這樣,小悅接下了照顧小明的重任。雖然她很忙,但是她覺得為了哥哥和小明,她要付出更多的努力和時間。接下來的幾天,小悅事無鉅細地照顧著小明,給他做飯、洗衣、輔導作業,盡職盡責地扮演著一個姑姑的角色。

在週末的下午,小悅坐在電腦前,專注地處理著工作。她穿著舒適的衣褲,一頭長髮隨意紮起,給人一種親切自然的感覺。突然,門口傳來了敲門聲,小悅放下手中的工作,開啟門一看,原來是她的侄兒小明。

小明是個活潑好動的孩子,二年級的他還保留著童年的純真和好奇。他瞪大了眼睛,好奇地看著小悅的電腦螢幕:“姐姐,你在幹什麼呀?”小悅微微一笑,糾正道:“我是姑姑,不是姐姐。”小明尷尬地撓了撓頭,趕緊改口:“姑姑,你在幹什麼呀?”

小悅笑著回答:“我在寫程式呀。”

“程式?可以教我嗎?”小明充滿期待地問。

“當然可以,”小悅點點頭,她向小明介紹了一下程式設計的基本知識和規則,然後開始手把手地教他如何編寫簡單的遊戲程式。

小悅耐心地教導小明如何設計坦克模擬遊戲。她先向他介紹了遊戲的基本規則,然後手把手地教他如何用程式操作鍵盤上的十字鍵來控制螢幕中的坦克移動。在這個過程中,小悅始終保持著微笑,她溫柔的話語和細心的指導讓小明倍感親切。

時間在不知不覺中流逝,小悅的心裡也漸漸感到了疲憊。但每當看到小明在程式設計的世界中自由翱翔的時候,她的心裡就充滿了欣慰和自豪。小明也漸漸地開始明白了程式設計的奧秘,他的興趣被極大地激發了出來,每天都充滿了新的期待和挑戰。儘管這段日子很累很忙,但小悅覺得很有意義。因為她知道這段時間對小明來說是寶貴的,而她也有幸參與其中,這讓她感到無比的滿足和幸福。


 小悅使用物件導向的思想,設計了一個名為PlayerMovement的類,用於實時控制遊戲中玩家的移動。該類包含一個directions列表,用於儲存當前按下的方向鍵。Position屬性表示當前玩家的位置,Direction屬性表示當前玩家的方向。

演算法實現1:

 1 public class PlayerMovement
 2 {
 3     private List<Direction> directions = new List<Direction>();
 4 
 5     public Tile Position { get; private set; }
 6     public Direction Direction { get; private set; }
 7 
 8     public PlayerMovement(int x, int y)
 9     {
10         // 初始化玩家位置
11         this.Position = new Tile(x, y);
12     }
13 
14     public void Update()
15     {
16         // 獲取新的方向
17         this.directions = GetNewDirections(this.directions);
18         if (!this.directions.Any()) return;
19 
20         var currentDirection = this.directions.Last();
21         if (this.Direction != currentDirection)
22         {
23             // 更新玩家方向
24             this.Direction = currentDirection;
25         }
26         else
27         {
28             // 獲取新的位置
29             this.Position = GetNewPosition(this.Position, this.Direction);
30         }
31     }
32 
33     private static List<Direction> GetNewDirections(IEnumerable<Direction> current)
34     {
35         var newState = current.ToList();
36 
37         var currentDirections = GetCurrentDirections().ToList();
38 
39         var directionsToRemove = newState.Except(currentDirections).ToList();
40         directionsToRemove.ForEach(d => newState.Remove(d));
41 
42         var directionsToAdd = currentDirections.Except(newState).ToList();
43         directionsToAdd.ForEach(d => newState.Add(d));
44 
45         return newState;
46     }
47 
48     private static Tile GetNewPosition(Tile pos, Direction dir)
49     {
50         var newX = pos.X;
51         var newY = pos.Y;
52 
53         switch (dir)
54         {
55             case Direction.Left:
56                 newX--;
57                 break;
58             case Direction.Right:
59                 newX++;
60                 break;
61             case Direction.Down:
62                 newY--;
63                 break;
64             case Direction.Up:
65                 newY++;
66                 break;
67         }
68 
69         return new Tile(newX, newY);
70     }
71 
72     private static IEnumerable<Direction> GetCurrentDirections()
73     {
74         // 透過yield return獲取當前按鍵狀態並返回對應的方向
75         if (Input.GetState(Direction.Right)) yield return Direction.Right;
76         if (Input.GetState(Direction.Left)) yield return Direction.Left;
77         if (Input.GetState(Direction.Down)) yield return Direction.Down;
78         if (Input.GetState(Direction.Up)) yield return Direction.Up;
79     }
80 }

透過測試用例解釋執行過程:

 1       private PlayerMovement _player;
 2   
 3       private void TestEquality(Direction direction, int x, int y)
 4       {
 5           _player.Update();
 6   
 7           Assert.AreEqual(direction, _player.Direction);
 8           Assert.AreEqual(new Tile(x, y), _player.Position);
 9       }
10   
11       [Test(Description = "Basic Test 1")]
12       public void BasicTest1()
13       {
14           _player = new PlayerMovement(0, 0);
15           Input.Clear();
16 
17           Press(Direction.Down);
18 
19           TestEquality(Direction.Down, 0, 0);
20           TestEquality(Direction.Down, 0, -1);
21           TestEquality(Direction.Down, 0, -2);
22 
23           Press(Direction.Left);
24           Press(Direction.Right);
25 
26           TestEquality(Direction.Left, 0, -2);
27           TestEquality(Direction.Left, -1, -2);
28 
29           Release(Direction.Left);
30 
31           TestEquality(Direction.Right, -1, -2);
32 
33           Release(Direction.Right);
34 
35           TestEquality(Direction.Down, -1, -2);
36           TestEquality(Direction.Down, -1, -3);
37 
38           Release(Direction.Down);
39 
40           TestEquality(Direction.Down, -1, -3);
41       }

首先,我們建立了一個名為_playerPlayerMovement物件,並將其初始位置設定為(0, 0)。然後,我們清除了輸入狀態。

接下來,我們按下了向下方向鍵。這將觸發Input.GetState(Direction.Down)返回true,所以GetCurrentDirections函式將返回一個包含Direction.Down的集合。然後,GetNewDirections函式將檢查當前方向列表directions和新的方向集合之間的差異,並更新directions列表。由於directions列表只包含一個元素Direction.Down,所以它不會發生任何變化。

TestEquality方法中,我們首先呼叫了_player.Update()來更新玩家的狀態。根據當前方向Direction.Down,我們期望玩家的位置為(0, -1)。然後,我們使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。

接下來,我們再次呼叫_player.Update()來更新玩家的狀態。根據當前方向Direction.Down,我們期望玩家的位置為(0, -2)。我們再次使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。

然後,我們按下了向左和向右方向鍵。這將觸發Input.GetState(Direction.Left)Input.GetState(Direction.Right)都返回true,所以GetCurrentDirections函式將返回一個包含Direction.LeftDirection.Right的集合。然後,GetNewDirections函式將檢查當前方向列表directions和新的方向集合之間的差異,並更新directions列表。由於directions列表已經包含了Direction.Down,所以它將保持不變。

TestEquality方法中,我們再次呼叫了_player.Update()來更新玩家的狀態。根據當前方向Direction.Down,我們期望玩家的位置為(0, -2)。我們再次使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。

然後,我們再次呼叫了_player.Update()來更新玩家的狀態。根據當前方向Direction.Left,我們期望玩家的位置為(-1, -2)。我們再次使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。

接下來,我們釋放了向左方向鍵。這將觸發Input.GetState(Direction.Left)返回false,所以GetCurrentDirections函式將返回一個不包含Direction.Left的集合。然後,GetNewDirections函式將檢查當前方向列表directions和新的方向集合之間的差異,並更新directions列表。由於directions列表已經包含了Direction.DownDirection.Right,所以它將保持不變。

TestEquality方法中,我們再次呼叫了_player.Update()來更新玩家的狀態。根據當前方向Direction.Right,我們期望玩家的位置為(-1, -2)。我們再次使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。

然後,我們釋放了向右方向鍵。這將觸發Input.GetState(Direction.Right)返回false,所以GetCurrentDirections函式將返回一個不包含Direction.Right的集合。然後,GetNewDirections函式將檢查當前方向列表directions和新的方向集合之間的差異,並更新directions列表。由於directions列表已經包含了Direction.Down,所以它將保持不變。

TestEquality方法中,我們再次呼叫了_player.Update()來更新玩家的狀態。根據當前方向Direction.Down,我們期望玩家的位置為(-1, -2)。我們再次使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。

最後,我們再次呼叫了_player.Update()來更新玩家的狀態。根據當前方向Direction.Down,我們期望玩家的位置為(-1, -3)。我們再次使用Assert.AreEqual方法來檢查玩家的方向和位置是否與預期相符。


演算法實現2:

 1 public class PlayerMovement
 2 {
 3     public Tile Position { get; private set; }  // 玩家的位置
 4     public Direction Direction { get; private set; }  // 玩家的方向
 5     
 6     // 不同方向對應的座標變化
 7     private Dictionary<Direction, (int xDelta, int yDelta)> moves = new Dictionary<Direction, (int xDelta, int yDelta)>()
 8     {
 9         { Direction.Up, (0, 1) },
10         { Direction.Down, (0, -1) },
11         { Direction.Left, (-1, 0) },
12         { Direction.Right, (1, 0) }
13     };
14     
15     // 方向的排序順序
16     private Direction[] sortOrder = new[] {Direction.Up, Direction.Down, Direction.Left, Direction.Right};
17     
18     private Stack<Direction> directions = new Stack<Direction>();  // 當前的方向列表
19     
20     public PlayerMovement(int x, int y)
21     {
22         this.Position = new Tile(x, y);  // 初始化玩家的位置
23     }
24     
25     public void Update()
26     {
27         Console.WriteLine("Called update");  // 輸出除錯資訊
28         
29         // 獲取當前按下的方向鍵
30         var pressedDirections = Enum.GetValues(typeof(Direction)).Cast<Direction>().Where(x => Input.GetState(x));
31         
32         // 清除不再按下的方向鍵
33         while (directions.Count != 0 && !pressedDirections.Contains(directions.Peek())) {
34             directions.Pop();
35         }
36         
37         if (pressedDirections.Count() == 0) {
38             return;  // 如果沒有按下任何方向鍵,則直接返回
39         }
40         
41         // 獲取新按下的方向鍵,並按照排序順序進行排序
42         var newDirections = pressedDirections.Except(directions).OrderBy(x => Array.IndexOf(sortOrder, x)).ToArray();
43         
44         // 將新的方向鍵加入到方向列表中
45         for (int i = newDirections.Length - 1; i >= 0; i -= 1) {
46             directions.Push(newDirections[i]);
47         }
48         
49         Direction directionToMove = directions.Peek();  // 獲取要移動的方向
50         
51         if (directionToMove == this.Direction) {
52             // 如果要移動的方向與當前方向相同,則更新玩家的位置
53             (int xDelta, int yDelta) = moves[directionToMove];
54             this.Position = new Tile(this.Position.X + xDelta, this.Position.Y + yDelta);
55         }
56         else {
57             // 如果要移動的方向與當前方向不同,則更新玩家的方向
58             this.Direction = directionToMove;
59         }
60     }
61 }

演算法2的執行步驟如下:

  1. 獲取當前按下的方向鍵。
  2. 清除不再按下的方向鍵。
  3. 如果沒有按下任何方向鍵,則直接返回。
  4. 獲取新按下的方向鍵,並按照排序順序進行排序。
  5. 將新的方向鍵加入到方向列表中。
  6. 獲取要移動的方向。
  7. 如果要移動的方向與當前方向相同,則更新玩家的位置。
  8. 如果要移動的方向與當前方向不同,則更新玩家的方向。

優點:

  1. 實現簡單,易於理解。
  2. 可以處理多個方向鍵同時按下的情況。

缺點:

  1. 在處理多個方向鍵同時按下的情況時,可能會出現按鍵順序和預期不一致的情況。
  2. 對於複雜的移動邏輯,可能需要新增更多的程式碼來處理。

 

總結,這兩個PlayerMovement類都是物件導向的,但是它們的實現方式略有不同:

1. 資料封裝

演算法2類使用屬性來封裝位置和方向,而演算法1類也使用屬性來封裝位置和方向。兩者的資料封裝方式相同。

2. 程式碼組織

演算法2類使用一個Update方法來更新玩家位置和方向,而演算法1類也使用一個Update方法來更新玩家位置和方向。兩者的程式碼組織方式相同。

3. 程式碼複用

演算法2類使用了一個moves字典來儲存方向和移動量的對應關係,而演算法1類則沒有使用字典,而是使用了GetCurrentDirections方法和GetNewPosition方法來計算新的方向和位置。兩者的程式碼複用方式略有不同。

4. 可擴充套件性

演算法2類使用了一個sortOrder陣列來定義方向的排序,而演算法1類則沒有使用這種方式。演算法1類使用了GetNewDirections方法,利用yield return特性來計算新的方向,這種方式更加靈活和可擴充套件。

它們都具有良好的資料封裝和程式碼組織,以及可擴充套件性和程式碼複用性。其中,演算法1類更加靈活和可擴充套件,適用於更加複雜的場景,例如:

  1. 虛擬現實和擴增實境應用:在虛擬現實和擴增實境應用中,使用者通常需要透過手柄、控制器或手勢來控制虛擬物件的移動。PlayerMovement演算法可以用於處理使用者輸入的方向指令,並更新虛擬物件的位置和方向。

  2. 機器人控制:在機器人控制領域,PlayerMovement演算法可以用於處理機器人的移動指令。例如,將方向鍵對映到機器人的移動方向,並根據按鍵的順序來確定機器人的移動路徑。

  3. 自動駕駛系統:在自動駕駛系統中,PlayerMovement演算法可以用於處理車輛的移動指令。例如,將方向鍵對映到車輛的轉向角度,並根據按鍵的順序來確定車輛的轉向路徑。

  4. 智慧家居系統:在智慧家居系統中,PlayerMovement演算法可以用於處理家居裝置的移動指令。例如,將方向鍵對映到智慧燈泡的亮度和顏色,並根據按鍵的順序來調整燈泡的狀態。


測試用例:

  1 using NUnit.Framework;
  2 using System;
  3 using System.Collections.Generic;
  4 using System.Linq;
  5 
  6 namespace TopDownMovement
  7 {
  8   [TestFixture]
  9   public class SolutionTest
 10   {
 11       private PlayerMovement _player;
 12   
 13       private void TestEquality(Direction direction, int x, int y)
 14       {
 15           _player.Update();
 16   
 17           Assert.AreEqual(direction, _player.Direction);
 18           Assert.AreEqual(new Tile(x, y), _player.Position);
 19       }
 20   
 21       [Test(Description = "Basic Test 1")]
 22       public void BasicTest1()
 23       {
 24           _player = new PlayerMovement(0, 0);
 25           Input.Clear();
 26 
 27           Press(Direction.Down);
 28 
 29           TestEquality(Direction.Down, 0, 0);
 30           TestEquality(Direction.Down, 0, -1);
 31           TestEquality(Direction.Down, 0, -2);
 32 
 33           Press(Direction.Left);
 34           Press(Direction.Right);
 35 
 36           TestEquality(Direction.Left, 0, -2);
 37           TestEquality(Direction.Left, -1, -2);
 38 
 39           Release(Direction.Left);
 40 
 41           TestEquality(Direction.Right, -1, -2);
 42 
 43           Release(Direction.Right);
 44 
 45           TestEquality(Direction.Down, -1, -2);
 46           TestEquality(Direction.Down, -1, -3);
 47 
 48           Release(Direction.Down);
 49 
 50           TestEquality(Direction.Down, -1, -3);
 51       }
 52 
 53       [Test(Description = "All keys at once")]
 54       public void BasicTest2()
 55       {
 56           _player = new PlayerMovement(0, 0);
 57           Input.Clear();
 58 
 59           Press(Direction.Down);
 60           Press(Direction.Left);
 61           Press(Direction.Right);
 62           Press(Direction.Up);
 63 
 64           TestEquality(Direction.Up, 0, 0);
 65           TestEquality(Direction.Up, 0, 1);
 66 
 67           Release(Direction.Left);
 68 
 69           TestEquality(Direction.Up, 0, 2);
 70 
 71           Release(Direction.Up);
 72 
 73           TestEquality(Direction.Down, 0, 2);
 74 
 75           Release(Direction.Down);
 76 
 77           TestEquality(Direction.Right, 0, 2);
 78           TestEquality(Direction.Right, 1, 2);
 79           TestEquality(Direction.Right, 2, 2);
 80 
 81           Release(Direction.Right);
 82 
 83           TestEquality(Direction.Right, 2, 2);
 84       }
 85 
 86       [Test(Description = "Random Test")]
 87       public void RandomTest()
 88       {
 89           int x, y;
 90           Random rand = new Random();
 91 
 92           x = rand.Next(200) - 100;
 93           y = rand.Next(200) - 100;
 94 
 95           _player = new PlayerMovement(x, y);
 96           Input.Clear();
 97 
 98           Press(Direction.Down);
 99           Press(Direction.Left);
100           Press(Direction.Right);
101           Press(Direction.Up);
102 
103           TestEquality(Direction.Up, x, y);
104 
105           for (int i = 0; i < rand.Next(20) + 1; i++)
106           {
107               y++;
108               TestEquality(Direction.Up, x, y);
109           }
110 
111           Release(Direction.Left);
112 
113           y++;
114           TestEquality(Direction.Up, x, y);
115 
116           Release(Direction.Up);
117 
118           TestEquality(Direction.Down, x, y);
119 
120           Release(Direction.Down);
121 
122           TestEquality(Direction.Right, x, y);
123           x++;
124           TestEquality(Direction.Right, x, y);
125           x++;
126           TestEquality(Direction.Right, x, y);
127 
128           Release(Direction.Right);
129 
130           TestEquality(Direction.Right, x, y);
131       }
132 
133       private void Press(Direction dir) { Console.WriteLine("Pressed " + dir); Input.Press(dir); }
134       private void Release(Direction dir) { Console.WriteLine("Released " + dir); Input.Release(dir); }
135   }
136 }