在实践中学习
上一期学习笔记:从零开始的 C# 学习笔记 ①: Microsoft Learn | CircleCrop Blog
这一次的学习笔记是完成代码后隔了一段时间才写的,看起来会更像一次回顾。
在实践中学习。枯燥地学习、记录知识点有些乏味的话,不妨试着上手写一个小程序,从实践中学得新知,增长经验。
这次,笔者尝试写一个控制台贪吃蛇小游戏,用来快速了解 C# 的基本语法和功能实现。
成果
完成代码:CSharp-Learning-Tests/Program.cs
编译程序:C-Sharp-Learning-Test.exe | CircleCrop WebDrive
项目概述
核心结构
变量声明
public const int snakeFullHealth = 1;
public static int snakeHealth = snakeFullHealth;
public static int snakeStartLength = 10;
public const int windowWidth = 20;
public const int windowHeight = 16;
// 便于修改围墙大小
public static int time = 0;
public static int score = 0;
public static char[,] map = new char[windowHeight, windowWidth];
// 0=blank, 1=snakebody, 2=snakehead, 3=apple, 4=wall
public static string? message;
public static Boolean gameOver = false;
public static string direction = "right";
public static Timer? timer;
public static List<(int x, int y)> snakeLocation = new List<(int x, int y)>();
// 蛇的位置,蛇头在最后一行
public static (int x, int y) appleLocation;
// 苹果坐标
private static int _snakeSpeed = 500; //ms
public static int snakeSpeed {
get {
return _snakeSpeed;
}
set {
if (value >= 60) {
_snakeSpeed = value;
}
}
}
snakeSpeed
使用了 C# 的属性,保证速度不会过高(刷新间隔 <=60ms
)
初始化
internal static void InitializingGameWindow() {
Console.OutputEncoding = Encoding.UTF8;
// 设定控制台编码以便输出 Emoji
message = "游戏开始!";
for (int i = 0; i < snakeStartLength; i++) {
snakeLocation.Add((0, i));
} // 开始将蛇放在左上角
RandomApple(); //初始化苹果
UpdateOnMap(); //刷新地图
RenderFrame(); //渲染到控制台
message = "";
}
internal static void RandomApple() {
Random random = new Random();
appleLocation = (random.Next(0, windowHeight), random.Next(0, windowWidth));
foreach (var x in snakeLocation) {
if (appleLocation == x) {
RandomApple();
}
} // 如果和蛇重叠就重新生成
}
按键监听
这部分还未掌握,是由 GPT 给出的。
if (Console.KeyAvailable) {
var key = Console.ReadKey(intercept: true).Key;
switch (key) {
case ConsoleKey.UpArrow:
case ConsoleKey.W:
ControlDirection("up");
break;
case ConsoleKey.DownArrow:
case ConsoleKey.S:
ControlDirection("down");
break;
case ConsoleKey.LeftArrow:
case ConsoleKey.A:
ControlDirection("left");
break;
case ConsoleKey.RightArrow:
case ConsoleKey.D:
ControlDirection("right");
break;
}
}
public static void ControlDirection(string newDirection) {
if ((newDirection == "up" && direction != "down") ||
(newDirection == "down" && direction != "up") ||
(newDirection == "left" && direction != "right") ||
(newDirection == "right" && direction != "left")) {
direction = newDirection;
} // 防止头转 180°
}
逻辑处理
地图刷新和逻辑处理写在一起了。
public static void UpdateMap() {
// 控制蛇的方向:每次根据当前方向更新蛇头的位置,并将其插入 List 的开头位置;
// 移除 List 的最后一个元素,以模拟前进的效果。
var lastSnakeHead = snakeLocation[snakeLocation.Count - 1];
switch (direction) {
case "up":
snakeLocation.Add((lastSnakeHead.x - 1, lastSnakeHead.y));
break;
case "down":
snakeLocation.Add((lastSnakeHead.x + 1, lastSnakeHead.y));
break;
case "left":
snakeLocation.Add((lastSnakeHead.x, lastSnakeHead.y - 1));
break;
case "right":
snakeLocation.Add((lastSnakeHead.x, lastSnakeHead.y + 1));
break;
}
var newSnakeHead = snakeLocation[snakeLocation.Count - 1];
// 苹果判定
if (newSnakeHead == appleLocation) {
score += 1;
snakeSpeed -= 20;
RandomApple();
} else {
snakeLocation.RemoveAt(0); //移除最后一个
}
UpdateOnMap();
/*foreach (var snake in snakeLocation) {
Console.WriteLine(snake.ToString());
} 调试用*/
// 位置更新完成,下面开始判定是否撞墙/自己
var positionSet = new HashSet<(int x, int y)>();
bool duplicatePosition = false;
foreach (var position in snakeLocation) {
// 如果添加失败,说明有重复位置,撞自己了
if (!positionSet.Add(position)) {
duplicatePosition = true;
break;
}
}
if (newSnakeHead.x == -1 || newSnakeHead.x == windowHeight || newSnakeHead.y == -1 || newSnakeHead.y == windowWidth || duplicatePosition) {
message = "游戏结束!";
RenderFrame();
gameOver = true;
Console.ReadLine();
} else {
message = "继续!";
}
}
控制台渲染
public static void RenderFrame() {
StringBuilder Frame = new StringBuilder();
// 构造生命值区域
string lifeArea = "生命: " + string.Concat(Enumerable.Repeat(Symbols.heart, snakeHealth))
+ string.Concat(Enumerable.Repeat(Symbols.blackHeart, snakeFullHealth - snakeHealth));
string timeArea = $"时间: {(time / 60):D2}:{(time % 60):D2}";
string scoreArea = "得分: " + score.ToString();
string lengthArea = "长度: " + snakeLocation.Count();
// 构造信息栏
Frame.AppendLine($"{lifeArea} {timeArea} {scoreArea} {lengthArea}");
Frame.AppendLine();
Frame.AppendLine("💬: " + message);
Frame.AppendLine();
// 顶部围墙
Frame.AppendLine(string.Concat(Enumerable.Repeat(Symbols.Wall, windowWidth + 2)));
// 构造地图
for (int i = 0; i < map.GetLength(0); i++) {
Frame.Append(Symbols.Wall);
for (int j = 0; j < map.GetLength(1); j++) {
switch (map[i, j]) {
// 0=blank, 1=snakebody, 2=snakehead, 3=apple, 4=wall
case (char)0:
Frame.Append(Symbols.Blank);
break;
case (char)1:
Frame.Append(Symbols.Body);
break;
case (char)2:
Frame.Append(Symbols.Head);
break;
case (char)3:
Frame.Append(Symbols.Apple);
break;
case (char)4:
Frame.Append(Symbols.Wall);
break;
}
}
Frame.AppendLine(Symbols.Wall); // 下一行
}
// 底部围墙
Frame.AppendLine(string.Concat(Enumerable.Repeat(Symbols.Wall, windowWidth + 2)));
Console.Clear(); // 清屏
Console.WriteLine(Frame.ToString()); // 渲染到控制台
}
学习总结
(以下含有 ChatGPT 生成内容,权当存档。我不是很想把要点复读一遍)
控制台渲染
Console.OutputEncoding
Console.Clear()
Console.WriteLine
Console.Write
区别在不换行。
属性
属性用于控制对私有变量的访问,提供了灵活的封装方式。
在 snakeSpeed
的实现中,通过 get
和 set
控制变量的读取与修改,并加以条件限制。
二维数列
二维数组在本项目中被用作地图 map
的数据结构,用于记录每个位置的状态(空白、墙体、蛇、苹果等)。
元组
public static (int x, int y) appleLocation;
StringBuilder
用于高效拼接字符串。
StringBuilder frame = new StringBuilder();
frame.AppendLine("游戏得分: " + score);
frame.AppendLine("蛇的长度: " + snakeLocation.Count);
Console.WriteLine(frame.ToString());
Comments NOTHING