第一个C# 程序

那么常规操作,第一步打印Hello World

1
2
3
4
5
6
7
8
9
10
11
12
using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}

这个写法是不使用顶级语句的,在C# 9.0引入了顶级语句,可以省略类和Main方法,直接写代码

注释

接下来看注释,注释与Java类似,分为单行注释多行注释

//单行注释

/* ... */多行注释

///用于注释类、方法、属性等,可以自动生成文档,这个是C#特有的,在类、方法上面敲入三个斜杠,即可自动生成注释模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 命名空间引用
using System;

// 命名空间定义(解决方案的名字)
namespace ConsoleApp1
{
// 类定义(类名)
class Program
{
/// <summary>
/// 程序入口
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
// 打印Hello, World!
Console.WriteLine("Hello, World!");
}
}
}

控制台相关内容

现在我们主要编写控制台应用,所有得来了解了解控制台相关的东西

输入输出

WriteLine输出并自动换行

Write输出但不换行

ReadLine获取输入,并自动换行,直到用户按下回车键

ReadKey获取输入,用户按下任意键就继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 打印Hello, World!(并自动空行)
Console.WriteLine("Hello, World!");

// 打印Hello(不自动空行)
Console.Write("Hello");

// 获取输入
Console.ReadLine();
Console.WriteLine("输入完毕");

// 获取输入(这个是按任意键就继续执行)
Console.ReadKey();

Console.WriteLine("按了任意键");

获取输入但不显示

如果你不希望用户输入的内容显示在控制台上,可以使用ReadKey(true)
这样用户输入的内容就不会显示在控制台上,而是存储在KeyChar属性中,返回为char类型

1
2
char c = Console.ReadKey(true).KeyChar; // 获取用户输入的字符,不显示在控制台上
Console.WriteLine("用户输入的字符是:" + c); // 输出用户输入的字符

清空控制台

Clear清空控制台

1
Console.Clear();

控制台窗口

设置控制台大小

SetWindowSize设置控制台大小,参数为

1
Console.SetWindowSize(80, 30);

设置控制台缓冲区大小

SetBufferSize设置控制台缓冲区大小,参数为

注意,缓冲区大小不能小于控制台大小,否则会报错

1
Console.SetBufferSize(80, 30);

退出控制台

Exit退出控制台

1
Console.Exit(0);

光标

设置光标位置

窗口中,左上角坐标为(0,0),向右为X轴正方向,向下为Y轴正方向

SetCursorPosition设置光标位置,参数为横坐标和纵坐标,单位为字符

1
Console.SetCursorPosition(10, 5); 

在视觉上,1y = 2x,所以要想让光标起始位置看起来像个正方形,就得将x坐标设置为y坐标的两倍

隐藏光标

CursorVisible设置光标是否可见,参数为布尔值

1
Console.CursorVisible = false;

颜色

文字颜色

ForegroundColor设置文字颜色,参数为枚举类型ConsoleColor,默认为白色

1
Console.ForegroundColor = ConsoleColor.Red;

背景颜色

BackgroundColor设置背景颜色,参数为枚举类型ConsoleColor,默认为黑色

注意设置好背景颜色之后,需要Clear一次,才能看到效果

1
2
Console.BackgroundColor = ConsoleColor.Blue;
Console.Clear();

折叠代码

与直接折叠大括号{}中的代码不同,C#提供了#region#endregion来折叠代码

你可以在代码中直接使用它们来自定义你要折叠的代码块

#region开头,以#endregion结尾,可以折叠代码,方便阅读

执行编译时,#region#endregion会被忽略,不会影响代码的执行

1
2
3
4
#region Region1
Console.WriteLine("Hello, World!");
#endregion
// 上面部分可以折叠为Region1

数据类型

好,接下来我们来看看C#的数据类型

有符号整型

有符号整型是可

sbyte -128~127,占用1字节

short -32768~32767,占用2字节

int -2147483648~2147483647,占用4字节

long -9223372036854775808~9223372036854775807,占用8字节

1
2
3
4
5
// 有符号的整型sbyte short int long
sbyte a = 100; // -128~127
short b = 100; // -32768~32767
int c = 100; // -2147483648~2147483647
long d = 100; // -9223372036854775808~9223372036854775807

无符号整型

那么顾名思义,它就是非负的整型

不过它所代表的数值的数量是和有符号整型一样的

byte 0~255,占用1字节

ushort 0~65535,占用2字节

uint 0~4294967295,占用4字节

ulong 0~18446744073709551615,占用8字节

1
2
3
4
5
// 无符号的整型byte ushort uint ulong
byte e = 100; // 0~255
ushort f = 100; // 0~65535
uint g = 100; // 0~4294967295
ulong h = 100; // 0~18446744073709551615

浮点型

浮点数,即小数,它分为单精度双精度

float 7~8位有效数字(有效数字:从第一位非零数字起算),需加f,占用4字节

double 15~16位有效数字,写小数时,默认为此类型,其他两个的需要指定,占用8字节

decimal 28~29位有效数字,需加m,占用16字节

1
2
3
4
// 浮点型 float double decimal
float i = 100.0f; // 7~8位有效数字
double j = 100.0; // 15~17位有效数字
decimal k = 100.0m; // 28~29位有效数字

字符型

char 只能存储一个字符,只能用单引号括起来,占用2字节

1
2
// 字符
char n = 'a'; // 只能存储一个字符

布尔型

bool true/false,占用1字节

1
2
3
// 布尔 
bool l = true;
bool m = false;

字符串

字符串类型的,一般用双引号括起来,可以存放多个字符,没有固定的占用字节数,根据内含字符的数量来决定的

string 字符串

1
2
// 字符串
string o = "hello world"; // 可以存储多个字符

变量初始化

一般来说,变量声明后就得初始化;亦可先定义,再初始化

1
int a = 100;

先定义,再初始化

1
2
int a;
a = 100;

但不论如何,使用变量之前必须初始化

另外,可以连续声明相同类型的变量,用逗号隔开

1
int a = 100, b = 200, c = 300;

变量命名规范

  • 变量名必须以字母、下划线开头
  • 变量名只能包含字母、数字、下划线,并区分大小写,不能重复
  • 变量名不能使用关键字
  • 变量名应当顾名思义
  • 一般遵循驼峰命名法,即首字母小写,后续每个单词首字母大写(函数、类可采用大驼峰命名法)

常量

常量,即不可改变的量,用const关键字定义,必须初始化,不能修改

1
2
const int months = 12;
Console.WriteLine("一年有{0}个月", months);

转义字符

转义字符,即用反斜杠\开头的字符,用于表示一些特殊字符

\n 换行
\t 制表符
\\ 斜杠
\" 双引号
\' 单引号
\b 光标退格
\0 空字符
\a 警报音

1
2
3
4
5
6
7
8
Console.WriteLine("换行符\n换行");
Console.WriteLine("制表符\t123");
Console.WriteLine("斜杠\\123");
Console.WriteLine("\"双引号\"");
Console.WriteLine("\'单引号\'");
Console.WriteLine("光标退格123\b123");
Console.WriteLine("空字符\0123");
Console.WriteLine("警报音\a");

类型转换

隐式转换

隐式转换为程序帮助我们转换,而不用我们手动转换

值得注意的是,数据类型——大范围装小范围

1
2
3
int a = 10;
long b = a; // 隐式转换
// short c = a; // 不能隐式转换,因为short范围 < int范围

在上面的例子中,a可以隐式转换为long类型的

a隐式转换为short类型的,因为short的范围小于int的范围

同样的,还有以下注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// decimal不能隐式转换double和float,但整型可以被转换
double d = 10.0;
// decimal e = d; // 不能隐式转换

// 有符号数不能隐式转换为无符号数
// uint f = a; // uint是无符号整数,int是有符号整数,不能隐式转换

// 无符号数隐式转换为有符号数,要求无符号数的范围必须小于有符号数的范围
uint g = 10;
long h = g; // 可以隐式转换,因为uint范围 < long范围

// 所有整型都可隐式转换为float、double
float i = a;
double j = b;

// string类型和bool型不能和其他类型隐式转换
// bool k = a; // 不能隐式转换

// 其他类型不能隐式转换为char,但char可以隐式转换为其他类型(string,小于int范围的也不能转换)
// char l = a; // 不能隐式转换
char m = 'a';
int n = m; // 其实是转换为ASCII码值

显式转换

显示转为为强制转换,用括号()

变量类型 变量名 = (目标类型)变量名

但要注意的是,强制转换可能导致溢出的问题,需要注意转换类型之间的范围

同时,其精度也有可能丢失

1
2
int o = 10;
short p = (short)o; // 显式转换

变量类型.Parse()方法可以将字符串转换为其他类型

1
2
3
4
5
int q = int.Parse("123");
// int r = int.Parse("123.45"); // 会报错,因为字符串中包含小数点

short s = short.Parse("123");
double t = double.Parse("123.45");

Convert.ToXXX()方法可以更准确地转换变量类型

每一种类型都有对应的Convert.ToXXX()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int u = Convert.ToInt32("123");
int v = Convert.ToInt32(123.45);

sbyte w = Convert.ToSByte("123");
short x = Convert.ToInt16("123");
int y = Convert.ToInt32("123");
long z = Convert.ToInt64("123");

byte aa = Convert.ToByte("123");
ushort bb = Convert.ToUInt16("123");
uint cc = Convert.ToUInt32("123");
ulong dd = Convert.ToUInt64("123");

float ee = Convert.ToSingle("123.45");
double ff = Convert.ToDouble("123.45");
decimal gg = Convert.ToDecimal("123.45");

bool hh = Convert.ToBoolean("true");
char ii = Convert.ToChar("a");

string jj = Convert.ToString(123);

所有类型都可以通过ToString()方法转换为字符串

1
2
int a1 = 123;
Console.WriteLine(a1.ToString());

另外,补充说明以下,Console.ReadLine()方法会读取用户输入的字符串,并返回一个string类型

异常捕获

在程序运行时,有可能会出现一些错误,导致程序无法正常运行

而有时候我们需要将出现的问题输出,更为具体地通俗易懂地告诉用户出现了什么问题(毕竟报错信息并不是所有人都看得懂的)

这个情况下,我们就要使用try-catch语句

try语句块中,我们放置可能会出现异常的代码,而catch语句块中,我们放置处理异常的代码

另外还有finally语句块,无论是否出现异常,都会执行finally语句块中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 // 基本语法
// 必须写的部分try-catch
try
{
// 放可能出现异常的代码
string a = Console.ReadLine();
int b = int.Parse(a);
Console.WriteLine(b);
}
catch (Exception ex)
{
// 当捕获到异常时执行的代码
Console.WriteLine("Failed");
}
// 可选写的部分finally
finally
{
// 无论是否发生异常都会执行的代码
}

运算符

赋值运算符

赋值运算符,即=,将右边的(表达式)值赋给左边的变量

1
2
3
int a = 10;
int b = 20;
b = b + 2 * 5 / 3 + 5 % 2;

不论=右边是什么,都是先运算右边的,再赋值给左边的变量

算术运算符

就像数学中的加减乘除,在程序中也有对应的

+ 加、- 减、* 乘、/ 除、% 取余(即除不尽的则取它的余数,除得尽的为0)

1
2
3
4
5
6
7
8
9
10
11
int a = 1;
int b = 2;
int c = a + b;

int d = a - b;

int e = a * b;

int f = a / b; // 如果两个整型相除,结果还是整型,且不会四舍五入

int g = a % b;

自增自减运算符

自增运算符,即++,将变量值加1;
自减运算符,即--,将变量值减1

但它们与变量的位置不同,结果也不同

1
2
3
4
5
int a = 1;
int b = a++; // 先赋值,再自增,即先将a的值赋给b,再将a加1
Console.WriteLine(a); // 2
int c = ++a; // 先自增,再赋值,即先将a加1,再将a的值赋给c
Console.WriteLine(a); // 3

复合运算符

它是=和算术运算符的结合,用于简化代码

+= 加等、-= 减等、*= 乘等、/= 除等、%= 取余等

1
2
3
4
5
6
7
8
9
a += b; // 等效 a = a + b;

a -= b; // 等效 a = a - b;

a *= b; // 等效 a = a * b;

a /= b; // 等效 a = a / b;

a %= b; // 等效 a = a % b;

关系运算符

关系运算符,用于比较两个值的大小关系,结果为bool类型

== 等于、!= 不等于、> 大于、< 小于、>= 大于等于、<= 小于等于

1
2
3
4
5
6
7
int h = 3;
Console.WriteLine(a == h);
Console.WriteLine(a != h);
Console.WriteLine(a > h);
Console.WriteLine(a < h);
Console.WriteLine(a >= h);
Console.WriteLine(a <= h);

任意数值可以直接进行比较

但特殊类型stringcharbool只能同类型进行==!=比较

逻辑运算符

逻辑运算符,用于连接多个条件,结果为bool类型

&& 逻辑与(and)、|| 逻辑或(or)、! 逻辑非(not)

1
2
3
Console.WriteLine(a > h && b > h);
Console.WriteLine(a > h || b > h);
Console.WriteLine(!(a > h));

其中,逻辑非!的优先级最高,逻辑与&&的优先级次之,逻辑或||的优先级最低

位运算符

位运算符,用于对二进制位进行操作,结果为int类型

& 按位与、| 按位或、^ 按位异或、~ 按位取反、<< 左移、>> 右移

1
2
3
4
5
6
7
8
9
10
11
12
int i1 = 2;
int j1 = 3;
Console.WriteLine(i1 & j1); // 2
Console.WriteLine(i1 | j1); // 3
Console.WriteLine(i1 ^ j1); // 1

// ~ 取反
Console.WriteLine(~i1); // -3
// << 左移,相当于乘以2的n次方
Console.WriteLine(i1 << 1); // 4
// >> 右移,相当于除以2的n次方
Console.WriteLine(i1 >> 1); // 1

三目运算符

表达式1 ? 表达式2 : 表达式3

如果表达式1为真,则返回表达式2的值;如果表达式1为假,则返回表达式3的值

1
2
3
int k = 1;
int l = 2;
int m = k > l ? k : l; // 2

字符串拼接

字符串拼接可以用算术运算符的+,将两个字符串连接在一起

1
2
string str = "hello";
Console.WriteLine(str + " world");

也可以使用{}占位符,对字符串进行格式化,占位符中放置从0开始的索引,表示要插入的变量

1
2
3
string str1;
str1 = String.Format("{0} world", str); // {}占位符
Console.WriteLine(str1);

括号

括号()用于改变运算符的优先级,括号内的表达式先运算

1
a = (c + d) * e;

运算优先级(只包括上面提到的)

  • 括号(最高优先级)
    () - 强制改变运算顺序

  • 自增/自减运算符
    x++ x-- (后缀)
    ++x --x (前缀)

  • 算数运算符
    * / % (乘、除、取模)
    + - (加、减)

  • 位运算符
    << >> (位移)
    & (按位与)
    ^ (按位异或)
    | (按位或)

  • 关系运算符
    < > <= >=

  • 相等运算符
    == !=

  • 逻辑运算符
    && (逻辑与)
    || (逻辑或)

  • 条件运算符(最低优先级)
    ?: (三元运算符)

流程控制

if语句

if语句用于根据条件执行不同的代码块

1
2
3
4
5
语法:
if (条件)
{
条件为真时执行的代码块
}
1
2
3
4
if (true)
{
Console.WriteLine("条件为真");
}

if-else语句

1
2
3
4
5
6
7
8
9
语法:
if (条件)
{
条件为真时执行的代码块
}
else
{
条件为假时执行的代码块
}
1
2
3
4
5
6
7
8
9
int a = 1;
if (a > 5)
{
Console.WriteLine("a大于5");
}
else
{
Console.WriteLine("a小于等于5");
}

if-else if-else语句

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:
if (条件1)
{
条件1为真时执行的代码块
}
else if (条件2)
{
条件2为真时执行的代码块
}
else
{
条件1和条件2都为假时执行的代码块
}
1
2
3
4
5
6
7
8
9
10
11
12
if (a > 5)
{
Console.WriteLine("a大于5");
}
else if (a == 5)
{
Console.WriteLine("a等于5");
}
else
{
Console.WriteLine("a小于5");
}

switch语句

switch类似于简化的if-else if-else语句,用于根据不同的条件执行不同的代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:
switch (表达式)
{
case 常量值1:
执行的代码块1
break;
case 常量值2:
执行的代码块2
break;
default:
执行的代码块3
break;
}

case判断的是表达式所得的值是否与所写的常量值一致,后面只能写常量,而不能写表达式或者变量

同样的,C#switch语句需要写break,不然会贯穿

即执行完一个case后,会继续执行下一个case,直到遇到break或者switch语句结束

1
2
3
4
5
6
7
8
9
10
11
12
13
a = 100;
switch (a)
{
case 1:
Console.WriteLine("a等于1");
break;
case 2:
Console.WriteLine("a等于2");
break;
default: // default可以省略
Console.WriteLine("a不等于1也不等于2");
break;
}

while循环

while循环用于在条件为时重复执行代码块

但要注意,如果条件永远为真,那么将导致死循环,直至内存溢出,程序崩溃

1
2
3
4
5
语法:
while (条件)
{
执行的代码块
}
1
2
3
4
5
6
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
i++;
}

break

break用于跳出循环,即当满足某个条件时,跳出循环,不再执行循环内的代码

1
2
3
4
5
6
7
8
9
while (true)
{
Console.WriteLine("请输入一个数字:");
string input = Console.ReadLine();
if (input == "1")
{
break;
}
}

continue

continue用于跳过本次循环,即当满足某个条件时,跳过本次循环,继续下一次循环

1
2
3
4
5
6
7
8
9
10
11
int a = 0;
while (a <= 20)
{
if (a % 2 == 0)
{
a++;
continue;
}
Console.WriteLine(a);
a++;
}

do-while循环

do-while循环与while循环类似,但它在循环体执行完毕后,再判断条件是否为真,
即无论如何都会执行一次循环体

1
2
3
4
5
语法:
do
{
执行的代码块
} while (条件);
1
2
3
4
5
6
int b = 0;
do
{
Console.WriteLine(b);
b++;
}while (b <= 10);

for循环

for循环用于在指定次数内重复执行代码块

1
2
3
4
5
语法:
for (初始化表达式; 条件表达式; 更新表达式)
{
执行的代码块
}
1
2
3
4
for (int c = 0; c < 10; c++)
{
Console.WriteLine(c);
}

控制台小项目#1-移动小方块

需求

控制台显示一个小方块,用户输入wsad,控制小方块上下左右移动

实现

控制台

首先我们设置一下控制台的相关内容

重新设置背景颜色,设置文字颜色,并隐藏光标

1
2
3
4
5
6
7
8
9
// 设置背景颜色
Console.BackgroundColor = ConsoleColor.Red;
Console.Clear();

// 设置文字颜色
Console.ForegroundColor = ConsoleColor.Yellow;

// 隐藏光标
Console.CursorVisible = false;

绘制小方块

接下来我们绘制小方块

1
2
3
4
5
int x = 0;
int y = 0;

Console.SetCursorPosition(x, y);
Console.Write("■");

不过,对于一个游戏来说,肯定是有循环层的,不然玩家输入一次那就没了可不行

Minecraft来说,有一个东西名为tick。它又分为随机刻游戏刻等。
游戏刻为20tick,即程序每秒将执行20次,以实时改变游戏中的一些东西,
如玩家位置、更新方块等。游戏的循环会一直持续下去,直到玩家退出游戏

所以,我们要将绘制小方块的代码放入循环语句中,一般我们就采用while (true)循环

1
2
3
4
5
6
7
8
int x = 0;
int y = 0;

while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");
}

获取用户输入

接下来,在控制小方块移动之前,我们得先获取用户输入

1
char c = Console.ReadKey(true).KeyChar;

这里我们使用ReadKey(true)不显示输入的字符,同时设置一个char c变量来接收用户输入的字符

移动小方块

接下来,我们根据用户输入的字符,来移动小方块

那么我们输入的字符只有WASDwasd8种,所以我们可以使用switch语句来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (c)
{
case 'w':
case 'W':
break;
case 's':
case 'S':
break;
case 'a':
case 'A':
break;
case 'd':
case 'D':
break;
}

这里我们就直接利用switch贯穿的特性,即同一个字符的大小写都由同一块代码块来执行

随后,我们在各个case中,根据用户输入的字符,来改变小方块的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
switch (c)
{
case 'w':
case 'W':
y -= 1;
break;
case 's':
case 'S':
y += 1;
break;
case 'a':
case 'A':
x -= 2;
break;
case 'd':
case 'D':
x += 2;
break;
}

好,现在我们输入WASD,小方块会根据输入的字符来移动,但是小方块不会消失,
所以我们需要在每次移动之前,将小方块原来的位置清空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");

char c = Console.ReadKey(true).KeyChar;

// 把之前的方块擦除
Console.SetCursorPosition(x, y);
Console.Write(" ");
switch (c)
{
...
}
}

擦除的话就直接写入空格即可,就可以将原来的位置的字符替换掉

限制移动范围

还没结束嘞!

当我们在在边缘位置移动小方块时,假设小方块此时贴在右边界
而我们没注意,继续按D(d),那么小方块就会超出控制台的范围,导致程序崩溃

所以,在各个case语句中,我们还得判断下一个坐标是否越界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
switch (c)
{
case 'w':
case 'W':
y -= 1;
if (y < 0)
{
y = 0;
}
break;
case 's':
case 'S':
y += 1;
if (y > Console.WindowHeight - 1)
{
y = Console.WindowHeight - 1;
}
break;
case 'a':
case 'A':
x -= 1;
if (x < 0)
{
x = 0;
}
break;
case 'd':
case 'D':
x += 1;
if (x > Console.WindowWidth - 1)
{
x = Console.WindowWidth - 1;
}
break;
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
namespace XM1
{
internal class Program
{
static void Main(string[] args)
{
Console.BackgroundColor = ConsoleColor.Red;
Console.Clear();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.CursorVisible = false;

int x = 0;
int y = 0;

while (true)
{
Console.SetCursorPosition(x, y);
Console.Write("■");

char c = Console.ReadKey(true).KeyChar;

Console.SetCursorPosition(x, y);
Console.Write(" ");
switch (c)
{
case 'w':
case 'W':
y -= 1;
if (y < 0)
{
y = 0;
}
break;
case 's':
case 'S':
y += 1;
if (y > Console.WindowHeight - 1)
{
y = Console.BufferHeight - 1;
}
break;
case 'a':
case 'A':
x -= 1;
if (x < 0)
{
x = 0;
}
break;
case 'd':
case 'D':
x += 1;
if (x > Console.WindowWidth - 1)
{
x = Console.BufferWidth - 1;
}
break;
}
}
}
}
}

控制台小项目#2-打Boss拿宝箱

需求

  • 小项目分为3个界面,开始界面游戏界面结束界面
  • 开始界面在上方中央位置显示给木,在下方显示开始游戏退出游戏,玩家使用WS进行选择,J键确认
  • 游戏界面中,边界有红色的代表边界墙,下方留出一块区域用于输出信息
  • 玩家用黄色的表示,用户输入WASD控制小方块移动,只能在边界墙的范围内移动
  • 生成一个Boss,用绿色的表示,当用户在Boss边上时,按J键进入对战状态
  • 对战状态时,玩家不能移动,按J键攻击,玩家随机给Boss造成伤害,而后Boss对玩家造成伤害
  • 当一方的攻击力小于对方防御力时,无法对对方造成伤害
  • 玩家血量小于等于0时,游戏结束,进入结束场景
  • Boss血量小于等于0时,游戏结束,玩家可以移动,生成一个宝箱,玩家到宝箱边上时,打开宝箱,游戏结束,进入结束场景
  • 结束场景上方中央位置根据输赢显示相应文字,在下方显示重新开始退出游戏,玩家使用w或s进行选择,j键确认

场景框架

这个小项目是比上面那个复杂得多了,不过不要捉急,我们拆解开一步步来

这个小项目是完全可以用上面已经提及的知识写出来的,未来会有更加复杂的小项目

前面我们已经写了一个控制玩家移动的小项目了,至少在玩家移动上是没有多大的问题了

现在,这个小项目分为了3个场景,除了第二个游戏场景需要实现一定的逻辑外,
其他两个场景还是比较简单的

显示的文字可以直接用控制台打印的方法输出,玩家输入字符也有相应的判断方法

但是场景间该怎么切换呢?流程控制,不过我们需要一个标识符变量)来判断当前处于哪个场景

好,那么到这里,我们大体上是可以开始写代码了,我们先为这3个场景打一个基本的框架,
再往里面填充东西

控制台基础设置

(以下代码均写在主函数Main中,毕竟现在也还没学到函数部分)

常规的还是控制台的一些设置,比如设置窗口大小文字颜色隐藏光标

1
2
3
4
5
6
7
8
9
#region 控制台基础设置
// 隐藏光标
Console.CursorVisible = false;
// 设置控制台大小
int w = 50;
int h = 30;
Console.SetWindowSize(w, h);
Console.SetBufferSize(w, h);
#endregion

场景循环判断

好了,接下来就是场景的部分了

上面说了,我们需要一个标识符用于判断当前的场景,所以我们就定义一个int类型的变量吧

1
int nowId = 1;

那么再后面,就是游戏的循环层了,也就是while(true),直到你退出游戏前,这个循环都不会停止

那么,在循环中,我们就得用流程控制来判断上面的标识符
再根据不同的标识符处理不同场景的逻辑

这里流程控制的话,我选择使用switch语句,因为的标识符是有限的,且可以枚举出来的

1
2
3
4
5
6
7
8
9
10
11
12
while (true)
{
switch (nowId)
{
case 1:
break;
case 2:
break;
case 3:
break;
}
}

开始场景

那么接下来我们就来写第一个场景

不过在开始之前,建议是先执行清空控制台的命令,因为我们最后可能是会从第三个场景返回第一个场景的,
得把前面的场景给先清空

1
Console.Clear();

打印“给木”

接下来就是打印“给木”(其实就是Game的音译)了,我们得先设置光标位置
Console.SetCursorPosition方法

1
2
Console.SetCursorPosition(w / 2 - 2, 8);
Console.Write("给木");

这里我们将光标位置设置到当前窗口一半,再往左2个字符(因为中文字符是占2个的)的位置,
高度上暂且设置为8,具体的可以根据大家实际情况调整

打印选项

那么接下来就是打印开始游戏退出游戏这两个选项了

大致也和上面的类似

1
2
3
4
5
6
7
// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");

不过现在,我们启动调试,也就只是打印了文字,我们输入WS也是没有反应的,所以下一步就是要获取用户输入,并将用户当前选择的选项高亮显示换个颜色

获取用户输入

在获取用户输入之前,我们肯定得先写一个while循环,毕竟总不能按一次就没了吧

用户输入WS,选择选项,直到用户输入了J键,我们才跳出循环,进入相应的场景

1
2
3
4
5
6
7
8
9
10
while (true)
{
// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");
}

不过,我们还得要一个变量,用于记录用户当前选择的选项,然后用户按WS时,改变这个变量的值

当然,这个变量的值我们不能让它无限地增大或者减少,所以在判断玩家输入的同时,
也要加个判断来限制它的范围

至于玩家的输入么,还是用switch来判断好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int nowSelectId = 0;

while (true)
{
// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");

char input = Console.ReadKey(true).KeyChar;

switch (input)
{
// 当玩家按下W(因为有大小写)
case 'w':
case 'W':
nowSelectId--;
// 超出范围时,限制范围
if (nowSelectId < 0)
{
nowSelectId = 0;
}
break;
// 同上
case 's':
case 'S':
nowSelectId++;
if (nowSelectId > 1)
{
nowSelectId = 1;
}
break;

case 'j':
case 'J':
// 按下J时,如果是“开始游戏”选项,则改变场景的标识符,进入游戏场景
if (nowSelectId == 0)
{
nowId = 2;
}
// 如果是“退出游戏”选项,则退出游戏(这里是直接控制台退出)
else
{
Environment.Exit(0);
}
break;
}
}

注意我们上面所说的变量,它是在while外面的,不然每次循环它都会被重置,那就没有意义了

不过现在,我们尚未实现让玩家当前选择的选项高亮显示,所以我们现在运行也不知道玩家选了什么

这里我们就要在各个选项打印之前来判断一下,选择的是否是这个选项,再改个颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Console.ForegroundColor = 
nowSelectId == 0 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

Console.ForegroundColor =
nowSelectId == 1 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");
...

这里我们运用三目运算符来进行判断,以开始游戏为例,
如果当前选择的选项是开始游戏,即选项的标识符为0,那么我们就将颜色设置为红色,否则就是白色

下面的退出游戏同理

那么现在进行测试,我们按下WS就可以看到选项在变化了,不过,当我们在开始游戏按下J之后,还在当前界面

为什么呢?因为我们没有退出这个场景的循环,即便你在switch的最后一个caseif中加入break,也还是不行

因为在switch中的break,只是跳出switch,并不会跳出while,所以我们还需要一个标识符,判断是否要跳出while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
while (true)
{
// isQuitWhile用于判断是否退出当前while循环
bool isQuitWhile = false;

...
switch (input)
{
...
case 'j':
case 'J':
if (nowSelectId == 0)
{
nowId = 2;
isQuitWhile = true;
}
else
{
Environment.Exit(0);
}
break;
}
if (isQuitWhile)
{
break;
}
}

这里我们添加一个bool变量类型的isQuitWhile,默认为false

当玩家按下J时,改变它的值,再在switch的外面,while循环内,
判断一下,如果为true,则跳出循环

OK,那么第一个部分就写完了,下面是第一部分的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#region 开始菜单

Console.Clear();
Console.SetCursorPosition(w / 2 - 2, 8);
Console.Write("给木");

int nowSelectId = 0;

while (true)
{
// isQuitWhile用于判断是否退出当前while循环
bool isQuitWhile = false;

// 根据选择的编号设置对应选项的颜色
Console.ForegroundColor =
nowSelectId == 0 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“开始游戏”
Console.SetCursorPosition(w / 2 - 4, 10);
Console.Write("开始游戏");

Console.ForegroundColor =
nowSelectId == 1 ? ConsoleColor.Red : ConsoleColor.White;

// 设置“退出游戏”
Console.SetCursorPosition(w / 2 - 4, 12);
Console.Write("退出游戏");

// 获取用户输入
char input = Console.ReadKey(true).KeyChar;
// 根据用户输入进行判断
switch (input)
{
case 'w':
case 'W':
nowSelectId--;
if (nowSelectId < 0)
{
nowSelectId = 0;
}
break;
case 's':
case 'S':
nowSelectId++;
if (nowSelectId > 1)
{
nowSelectId = 1;
}
break;
// 用户选择J则进行该选项
case 'j':
case 'J':
if (nowSelectId == 0)
{
nowId = 2;
isQuitWhile = true;
}
else
{
Environment.Exit(0);
}
break;
}
if (isQuitWhile)
{
break;
}
#endregion
...

游戏场景

那么接下来,我们来讲游戏场景的代码

首先还是常规的,先清空控制台,因为我们切换到另外一个场景了,得把前一个场景的内容都清空

1
Console.Clear();

接下来,我们一步步来实现上面的需求

绘制边界墙

在需求中,要求我们绘制边界墙,用红色的表示

那么边界墙,就是围着屏幕范围画一圈,可以用for循环来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 调整颜色
Console.ForegroundColor = ConsoleColor.Red;

// 三道横墙
for (int i = 0; i < w; i+=2)
{
Console.SetCursorPosition(i, 0);
Console.Write("■");

Console.SetCursorPosition(i, h - 6);
Console.Write("■");

Console.SetCursorPosition(i, h - 1);
Console.Write("■");
}

h是前面写到的屏幕高度,w是屏幕宽度

h-1是为了防止越界,i的步长为2,也是因为这个小方块占2个字符宽度

这里我们一共绘制3道横墙,第一道墙在屏幕顶部,第二道墙在距离屏幕底部上方5个字符处,
第三道墙在屏幕底部,在第二道和第三道墙之间是打印信息的区域

接下来是绘制竖墙,竖墙的绘制和横墙的绘制类似,只不过横墙是i+=2,竖墙是i++

1
2
3
4
5
6
7
8
9
// 两道竖墙
for (int i = 0; i < h; i++)
{
Console.SetCursorPosition(0, i);
Console.Write("■");

Console.SetCursorPosition(w - 2, i);
Console.Write("■");
}

竖墙就第一列和最后一列要绘制

现在我们测试,进入游戏场景时,就可以看到红色的边界墙了

绘制玩家

那么边界墙已经绘制好了,接下来就是绘制玩家和Boss了

不过要注意的是,因为后面玩家要移动,还有攻击逻辑,所以我们要将它们放在循环中

我们先绘制玩家,玩家用黄色的表示,至于一开始玩家要生成的位置,我们可以直接指定一个位置

不过,如果你要高级一点的话则可以使用Random,来随机给定一个位置,这里我们就指定了

1
2
3
4
5
6
7
8
9
10
while (true)
{
int playerX = 4;
int playerY = 5;
string playerId = "●";
ConsoleColor playerColor = ConsoleColor.Yellow;
Console.ForegroundColor = playerColor;
Console.SetCursorPosition(playerX, playerY);
Console.Write(playerId);
}

这里我们定义了一个playerXplayerY,分别表示玩家在屏幕中的位置

再定义一个玩家的颜色,最后根据位置和颜色信息来绘制玩家,现在当我们进入第二个场景时,
就可以看到一个黄色小球了

不过,或许有人会问,为什么墙的绘制没有放在循环中,而是放在了循环外面呢?

因为墙的位置是固定的,不会改变,所以放在循环外面,
而玩家和Boss的位置和状态(当然在这里我们的Boss是不会动的,但是会死亡)是会改变的,
所以放在循环中

绘制Boss

那么玩家已经绘制好了,接下来就是绘制Boss了,也和玩家绘制差不多

Boss用绿色的表示,位置我们也指定一下好了

1
2
3
4
5
6
7
int bossX = 24;
int bossY = 15;
string bossId = "■";
ConsoleColor bossColor = ConsoleColor.Green;
Console.ForegroundColor = bossColor;
Console.SetCursorPosition(bossX, bossY);
Console.Write(bossId);

随后我们就可以看到一个绿色的小方块

控制玩家移动

那么,接下来就来到游戏的循环层,要获取用户的输入,控制玩家移动

这个东西我们在上面已经写过了,所以这里我们就直接来写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
switch (input2)
{
case 'w':
case 'W':
playerY--;
if (playerY < 1)
{
playerY = 1;
}
break;
case 's':
case 'S':
playerY++;
if (playerY > h - 7)
{
playerY = h - 7;
}
break;
case 'a':
case 'A':
playerX -= 2;
if (playerX < 2)
{
playerX = 2;
}
break;
case 'd':
case 'D':
playerX += 2;
if (playerX > w - 4)
{
playerX = w - 4;
}
break;
case 'j':
case 'J':
break;
}

复杂数据类型

枚举

枚举是一种数据类型,用于定义一组命名常量(其值为整型),一般用来表示状态、类型等等

声明

枚举的声明方式如下:

1
2
3
4
5
6
enum 枚举名
{
枚举项1,
枚举项2,
...
}

枚举名可以加上E以区分其他数据类型,具体使用:

1
2
3
4
5
6
7
enum EColor
{
BLACK, // 其值为0
RED, // 其值为1
GREEN = 10, // 其值为10
BLUE // 其值为11(在上一个基础上累加)
}

枚举值从0开始,也可以手动指定枚举值,后面的枚举值会在此基础上累加

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明枚举变量
// 自定义枚举类型 变量名字 = 自定义枚举类型.枚举成员
EColor color = EColor.RED;
switch (color)
{
case EColor.BLACK:
Console.WriteLine("黑色");
break;
case EColor.RED:
Console.WriteLine("红色");
break;
case EColor.GREEN:
Console.WriteLine("绿色");
break;
case EColor.BLUE:
Console.WriteLine("蓝色");
break;
}

类型转换

因为枚举类型中的枚举项实际上是整型常量,所以枚举类型可以和整型进行转换,转换方式如下:

1
2
3
// 枚举的类型转换(int)
int i = (int)EColor.RED;
Console.WriteLine(i);

同样枚举也可以转换为string类型

1
2
3
// 枚举的类型转换(string)
string s = EColor.RED.ToString();
Console.WriteLine(s); // 输出:RED

string类型也可以转换为枚举类型

其语法为

1
目标枚举类型 变量名 = (目标枚举类型)Enum.Parse(typeof(目标枚举类型), "枚举成员");

使用示例

1
2
EColor color2 = (EColor)Enum.Parse(typeof(EColor), "GREEN");
Console.WriteLine(color2); // 输出:GREEN

数组

数组是一种数据类型,用于存储一组相同类型的数据,其长度是固定的,不能改变

数组有一维数组、二维数组、多维数组、交错数组等等

一维数组

声明

一维数组的声明方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
数据类型[] 数组名;
int[] arr1;

数据类型[] 数组名 = new 数据类型[数组长度];
int[] arr2 = new int[5];

数据类型[] 数组名 = new 数据类型[] { 数据1, 数据2, 数据3, ... };(这个比较常用)
int[] arr5 = new int[] { 1, 2, 3, 4, 5 };

数据类型[] 数组名 = { 数据1, 数据2, 数据3, ... };
int[] arr3 = { 1, 2, 3, 4, 5 };

数据类型[] 数组名 = new 数据类型[数组长度] { 数据1, 数据2, 数据3, ... };
int[] arr4 = new int[5] { 1, 2, 3, 4, 5 };

数组长度获取

获取数组长度可以使用Length属性

1
2
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr.Length); // 输出:5

数组元素访问、修改

数组元素可以通过索引来访问,索引从0开始,但索引不得越界数组长度 - 1

1
2
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr[0]); // 输出:1

同样可以利用这种方法来修改数组元素

1
2
3
int[] arr = { 1, 2, 3, 4, 5 };
arr[0] = 10;
Console.WriteLine(arr[0]); // 输出:10

数组遍历

数组遍历可以使用for循环,也可以使用foreach循环

1
2
3
4
5
6
7
8
9
10
11
int[] arr = { 1, 2, 3, 4, 5 };
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}

int[] arr2 = { 1, 2, 3, 4, 5 };
foreach (int j in arr2)
{
Console.WriteLine(j);
}

增加元素

由于数组初始化之后就不能再添加新元素,我们要使用搬家的方法来实现

即重新定义一个更长长度的数组,将原数组中的元素复制到新数组中,最后将新数组赋值给原数组

1
2
3
4
5
6
7
8
int[] arr3 = { 1, 2, 3, 4, 5 };
int [] arr6 = new int[6];
// 搬家
for (int i = 0; i < arr3.Length; i++)
{
arr6[i] = arr3[i];
}
arr3 = arr6;

删除元素

删除元素同样如此

1
2
3
4
5
6
7
int[] arr3 = { 1, 2, 3, 4, 5 };
int[] arr7 = new int[4];
for (int i = 0; i < arr7.Length; i++)
{
arr7[i] = arr3[i];
}
arr3 = arr7;

但其实在实际开发过程中,我们并不会这么干,后续会接触到更好的方法来处理数组

查找元素

查找元素可以使用for循环遍历数组

1
2
3
4
5
6
7
for (int i = 0; i < arr3.Length; i++)
{
if (arr3[i] == 10)
{
Console.WriteLine(i);
}
}

二维数组

声明

声明上与一维数组差不多,只不过需要多一个维度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
数据类型[,] 数组名;
int[,] arr;

数据类型[,] 数组名 = new 数据类型[行数, 列数];
int[,] arr2 = new int[3, 4];

数据类型[,] 数组名 = new 数据类型[,] { { 数据1, 数据2, 数据3, ... }, { 数据1, 数据2, 数据3, ... }, ... };
int[,] arr3 = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };

数据类型[,] 数组名 = { { 数据1, 数据2, 数据3, ... }, { 数据1, 数据2, 数据3, ... }, ... };
int[,] arr4 = { { 1, 2, 3 }, { 4, 5, 6 } };

数据类型[,] 数组名 = new 数据类型[行数, 列数] { { 数据1, 数据2, 数据3, ... }, { 数据1, 数据2, 数据3, ... }, ... };
int[,] arr5 = new int[3, 4] { { 1, 2, 3 }, { 4, 5, 6 } };

数组长度获取

对于二维数组来说,GetLength()方法返回的是数组的总元素个数,而GetLength(0)和GetLength(1)分别返回的是数组的行数和列数

1
2
3
int[,] arr = new int[3, 4];
Console.WriteLine(arr.GetLength(0)); // 输出:3
Console.WriteLine(arr.GetLength(1)); // 输出:4

数组元素访问、修改

二维数组可以通过行索引列索引来访问、修改元素

1
2
3
int[,] arr = new int[3, 4];
arr[0, 0] = 10;
Console.WriteLine(arr[0, 0]); // 输出:10

数组遍历

二维数组可以通过for循环来遍历,但是因为有两个维度,需要嵌套两个for循环

1
2
3
4
5
6
7
8
int[,] arr = {{ 1, 2, 3 }, { 4, 5, 6 } };
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
Console.WriteLine(arr[i, j]);
}
}

增加元素

二维数组同样需要使用搬家的方法来实现增加元素

1
2
3
4
5
6
7
8
9
int[,] arr5 = new int[3, 3];
for (int i = 0; i < arr4.GetLength(0); i++)
{
for (int j = 0; j < arr4.GetLength(1); j++)
{
arr5[i, j] = arr4[i, j];
}
}
arr4 = arr5;

删除元素

1
2
3
4
5
6
7
8
9
int [,] arr6 = new int[2, 2];
for (int i = 0; i < arr6.GetLength(0); i++)
{
for (int j = 0; j < arr6.GetLength(1); j++)
{
arr6[i, j] = arr4[i, j];
}
}
arr4 = arr6;

查找元素

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < arr4.GetLength(0); i++)
{
for (int j = 0; j < arr4.GetLength(1); j++)
{
if (arr4[i, j] == 100)
{
Console.WriteLine("找到了");
}
}
}

交错数组

交错数组是数组的数组,即二维数组中的每一行都是一个一维数组

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数据类型[][] 数组名;
int[][] arr;

数据类型[][] 数组名 = new 数据类型[行数][]; // 声明行数,列数不声明,因为列数可能不同
int[][] arr2 = new int[3][];

int[][] jaggedArray2 = new int[3][] {new int[] {1,2,3},
new int[] {4,5},
new int[] {6}};

int[][] jaggedArray3 = new int[][] { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };

int[][] jaggedArray4 = { new int[] { 1, 2, 3 },
new int[] { 4, 5 },
new int[] { 6 } };

数组长度获取

GetLength(0)返回的是数组的行数,但列数要先获取某一行的值,再通过Length获取

1
2
Console.WriteLine(jaggedArray4.GetLength(0)); // 行数
Console.WriteLine(jaggedArray4[0].Length); // 第一行的列数

数组元素访问、修改

1
2
3
4
5
// 获取元素
Console.WriteLine(jaggedArray4[1][1]); // 5

// 修改
jaggedArray4[1][1] = 100;

数组遍历

1
2
3
4
5
6
7
for (int i = 0; i < jaggedArray4.GetLength(0); i++)
{
for (int j = 0; j < jaggedArray4[i].Length; j++)
{
Console.Write(jaggedArray4[i][j] + " ");
}
}

后面的增删查都与前面的差不多,这里就不再赘述