【算法】游戏中的学习,使用c#面向对象特性控制游戏角色移动
最近,小悦的生活像是一首繁忙的交响曲,每天忙得团团转,虽然她的日程安排得满满当当,但她并未感到充实。相反,她很少有时间陪伴家人,这让她感到有些遗憾。在周五的午后,小悦的哥哥突然打来电话,他的声音里充满了焦虑。
“小悦,我有个事情想拜托你。”哥哥的声音传来。
小悦不禁有些疑惑,哥哥有什么事情需要她帮忙呢?她忍不住问:“哥哥,有什么需要我帮忙的吗?”
哥哥解释说:“我最近要出差一段时间,大概一个星期左右。而我的妻子目前正在照顾住院的父母,没有时间照顾小明。我想请你帮忙照顾小明一段时间。”
小悦愣住了,没想到哥哥会把这个重任托付给她。这是一个不小的挑战,她有些担心自己无法胜任。但是,她知道她不能让自己的感情影响到哥哥的决定。于是她坚定地回答:“好的,哥哥,我会照顾好小明的。你放心去出差吧。”
电话那头的哥哥松了一口气,感激地说:“谢谢你,小悦。我相信你会照顾好小明的。我会尽快回来的。”
就这样,小悦接下了照顾小明的重任。虽然她很忙,但是她觉得为了哥哥和小明,她要付出更多的努力和时间。接下来的几天,小悦事无巨细地照顾着小明,给他做饭、洗衣、辅导作业,尽职尽责地扮演着一个姑姑的角色。
在周末的下午,小悦坐在电脑前,专注地处理着工作。她穿着舒适的衣裤,一头长发随意扎起,给人一种亲切自然的感觉。突然,门口传来了敲门声,小悦放下手中的工作,打开门一看,原来是她的侄儿小明。
小明是个活泼好动的孩子,二年级的他还保留着童年的纯真和好奇。他瞪大了眼睛,好奇地看着小悦的电脑屏幕:“姐姐,你在干什么呀?”小悦微微一笑,纠正道:“我是姑姑,不是姐姐。”小明尴尬地挠了挠头,赶紧改口:“姑姑,你在干什么呀?”
小悦笑着回答:“我在写程序呀。”
“程序?可以教我吗?”小明充满期待地问。
“当然可以,”小悦点点头,她向小明介绍了一下编程的基本知识和规则,然后开始手把手地教他如何编写简单的游戏程序。
小悦耐心地教导小明如何设计坦克模拟游戏。她先向他介绍了游戏的基本规则,然后手把手地教他如何用程序操作键盘上的十字键来控制屏幕中的坦克移动。在这个过程中,小悦始终保持着微笑,她温柔的话语和细心的指导让小明倍感亲切。
时间在不知不觉中流逝,小悦的心里也渐渐感到了疲惫。但每当看到小明在编程的世界中自由翱翔的时候,她的心里就充满了欣慰和自豪。小明也渐渐地开始明白了编程的奥秘,他的兴趣被极大地激发了出来,每天都充满了新的期待和挑战。尽管这段日子很累很忙,但小悦觉得很有意义。因为她知道这段时间对小明来说是宝贵的,而她也有幸参与其中,这让她感到无比的满足和幸福。
小悦使用面向对象的思想,设计了一个名为PlayerMovement的类,用于实时控制游戏中玩家的移动。该类包含一个directions列表,用于存储当前按下的方向键。Position属性表示当前玩家的位置,Direction属性表示当前玩家的方向。
算法实现1:
1 public enum Direction { Up = 8, Down = 2, Left = 4, Right = 6 } 2 3 public struct Tile 4 { 5 public int X { get; } 6 public int Y { get; } 7 8 public Tile(int x, int y); 9 } 10 11 public static class Input 12 { 13 // pressed = true, released = false 14 public static bool GetState(Direction direction); 15 }
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 }
首先,我们创建了一个名为_player
的PlayerMovement
对象,并将其初始位置设置为(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.Left
和Direction.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.Down
和Direction.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的运行步骤如下:
- 获取当前按下的方向键。
- 清除不再按下的方向键。
- 如果没有按下任何方向键,则直接返回。
- 获取新按下的方向键,并按照排序顺序进行排序。
- 将新的方向键加入到方向列表中。
- 获取要移动的方向。
- 如果要移动的方向与当前方向相同,则更新玩家的位置。
- 如果要移动的方向与当前方向不同,则更新玩家的方向。
优点:
- 实现简单,易于理解。
- 可以处理多个方向键同时按下的情况。
缺点:
- 在处理多个方向键同时按下的情况时,可能会出现按键顺序和预期不一致的情况。
- 对于复杂的移动逻辑,可能需要添加更多的代码来处理。
总结,这两个PlayerMovement类都是面向对象的,但是它们的实现方式略有不同:
1. 数据封装
算法2类使用属性来封装位置和方向,而算法1类也使用属性来封装位置和方向。两者的数据封装方式相同。
2. 代码组织
算法2类使用一个Update方法来更新玩家位置和方向,而算法1类也使用一个Update方法来更新玩家位置和方向。两者的代码组织方式相同。
3. 代码复用
算法2类使用了一个moves字典来存储方向和移动量的对应关系,而算法1类则没有使用字典,而是使用了GetCurrentDirections方法和GetNewPosition方法来计算新的方向和位置。两者的代码复用方式略有不同。
4. 可扩展性
算法2类使用了一个sortOrder数组来定义方向的排序,而算法1类则没有使用这种方式。算法1类使用了GetNewDirections方法,利用yield return特性来计算新的方向,这种方式更加灵活和可扩展。
它们都具有良好的数据封装和代码组织,以及可扩展性和代码复用性。其中,算法1类更加灵活和可扩展,适用于更加复杂的场景,例如:
-
虚拟现实和增强现实应用:在虚拟现实和增强现实应用中,用户通常需要通过手柄、控制器或手势来控制虚拟对象的移动。PlayerMovement算法可以用于处理用户输入的方向指令,并更新虚拟对象的位置和方向。
-
机器人控制:在机器人控制领域,PlayerMovement算法可以用于处理机器人的移动指令。例如,将方向键映射到机器人的移动方向,并根据按键的顺序来确定机器人的移动路径。
-
自动驾驶系统:在自动驾驶系统中,PlayerMovement算法可以用于处理车辆的移动指令。例如,将方向键映射到车辆的转向角度,并根据按键的顺序来确定车辆的转向路径。
-
智能家居系统:在智能家居系统中,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 }