跳至正文

C#基础知识(1)

欢迎来到C#基础知识介绍,其实很多C#语法都可以在网上找到,这里不会详细介绍这些基础(因为实在太多了,没法在两章全部覆盖),所以我建议大家自己先从网上找到一些C#教程并对着这些教程学习。裙中世界主要是将这些C#知识与Mod联系起来,让你们知道这些语法知识在Mod制作里面都有实际的用处。

书籍推荐:《C#入门经典》,MSDN


基本数据类型

通过前几章的魔改物品属性,我们会发现对于每个物品的属性来说,我们想要修改的值在代码里有不同的表示方式,比如:

// 伤害的数值,是一个数字
item.damage = 123;
// 击退的数值,是一个小数,后面跟了一个f(为啥要这样呢?)
item.knockBack = 0.25f;
// 只有是和否的属性,要么设置为true要么false
item.autoReuse = false;
// 这……,如果你善用VS会发现它等同于数字
item.UseSound = SoundID.Item36;
// 手枪是个名字,为什么一定要被引号引起来?
DisplayName.SetDefault("手枪");
// AddIngredient为什么一定要这么写?
recipe1.AddIngredient(ItemID.IronBar, 10);

以后我们会看到,还有更多更怪异的的属性设置方式。那么这些值到底代表什么意思,什么时候应该用什么样的值呢?不要着急,我们慢慢讲。

整数

我们在VS中把鼠标移到item.damage以及它后面的数字上

可以看到,当鼠标移上去以后,提示了一段文字(字段) int Item.damage,这段话什么意思呢?我先要解释一下(字段)是什么。 字段就是一个类型的属性,比如这个类型是Item(物品),用通俗的语言说就是,这个物品(英文:Item)有一个属性叫做damage(伤害)。这个.就代表了“的”的意思,物品的伤害属性(Item.damage)。

接下来我们重点关注int,首先它是个整数类型,也就是说,只能用-1, 0, 35, 114514来表示这样的数字。注意到第二张图我们有一个32位带符号整数。首先32位代表这个整数是用32个比特来表示的,带符号的意思就是可以是负数,这样的数据有一个范围\([-2^{31}, 2^{31}-1]\),如果你设置了一个很大的数很有可能实际上变成了负数。那么为什么会这样呢?我有点想讲但是这可能一章都讲不完QAQ。所以还是留到第四部分再讲吧。除此之外整数还有byte, shortlong等等类型,他们代表的不同的位数。

浮点型(小数)

浮点型,就是带着小数的数字,如-1e18, 1.0, 114.514f,因为整数型是没有办法带小数的,所以C#引入了小数类型来方便计算。同样我们还是来看一下VS里面的效果:

我们可以看到,这个属性是一个float类型,并且在C#中,这个类型叫做单精度浮点数字,那么什么是单精度呢?就是说正浮点数是用32位表示的,精度范围比较低。那么有单精度肯定就有双精度了(不要问啥是三精度)。double类型就是双精度,他用64位来表示一个浮点数,比float精度更高,范围更大。

值得一提的是,float的值一定要加一个f,如果你不加,那么C#就会认为这个数值是double类型,但是XNA系统都是依靠float的,所以这点要注意。

什么是精度呢,因为计算机中表示一个小数是不能完全表示全的,比如 \(3.141592653\cdots\) ,你总不可能把所有位数都显示出来吧,给你整个宇宙都装不下的。这时候就要强制削减小数的精准度,取一个近似这个小数,能在计算机中表示的小数值,至于这个近似的数和原来的小数有多大偏差,我们就叫它精度double的精度比float高很多(不一定是两倍)。 至于计算机怎么做到表示小数的,留到第四部分讲,我可以告诉你们它跟你们想的可能完全不同。

那就奇怪了,double既然比float要好,为什么不用double呢。那是因为,double占用内存占用的比float多一倍,而且对于2D图形渲染来说,不需要那么高的精准度,所以综合下来选择float更好。

如果你在给float类型的数据赋值的时候,漏掉了末尾的f会怎么样呢

会被VS画上红线,告诉你你使用了double而不是float。如果想把double类型强制降低精度变成float就要用强制转换

// 强制把双精度类型114.514转化为单精度
float XXX = (float)114.514;

float a = 1.0f;
a = (float)(a * 1.5);

浮点数的取值范围非常广,暂时不需要担心有上限,起码在TR里面。

布尔值(真假)

我们接下来在VS中把鼠标移到autoReuse

所谓布尔值,其实只是个名字而已,代表的含义就是这种数据类型只有两个选择,要么是(true),要么否(false)。这个大家心里应该有这个概念,了解一下就行了。 值得一提的是,bool值虽然只有0,1两个选项,但是占用的空间却有8位。

字符串类型(string)

用引号引起来的值就是字符串类型,它代表着一段文本。观察一下物品的名字设定:

// 物品的描述,加入换行符 '\n' 可以多行显示哦
Tooltip.SetDefault("What is this blade made of?\n" +
    "Ohh, Iron...");
// 同理,我们加一个中文的翻译(???我们不本来就是中国人?
Tooltip.AddTranslation(GameCulture.Chinese, "它是由什么做的?\n" +
    "哦铁啊,那没事了");

那么问题来了,\n到底是个什么东西呢,其实它代表的就是一个换行符,也就是按下回车键以后的效果。这种以\开头,与显示效果不同的符号,我们叫做转义字符,以后或许还会遇到更多的这样的字符。C#里字符编码是UTF-16。

那么字符串的组成部分是什么呢?你可能已经猜到了,是字符,也就是单个字'a', 'b', '\n'这样的,字符串其实是由零个或多个字符组成的。字符在C#里的类型是char

其他类型

如果说刚才的几种类型我们叫做基本值类型,也就是C#自带的值类型。那么接下来要讲的Vector2这个类型就不是基本值类型了,我们可以管它叫自定义值类型。比如说玩家的位置player.position就用到了这个类型

可以看到VS标记出的类型连颜色都变了,那么这个浅绿色的名字就代表这是一个自定义值类型。那么你可能注意到了,自己声明的类名以及玩家NPC等等,他们的颜色都是青色的,这是什么类型?

这就是引用类型了,你所声明的所有class(类)都是引用类型

我们的声音ID其实不是id,而是一个引用类型的实例,所以我们不能给它一个数字。

注意,不同自定义数据类型之间不能直接赋值,除非定义了转化的规则。但是某些基本数据类型之间是可以隐式转换的,比如floatdouble

这里有一张基本数据类型的表,我就不多做讲解了,有兴趣的同学自己研究一下吧。

类型 代表含义 取值范围
bool 布尔值 \(true\) 和 \(false\)
byte 8位无符号整数 \([0, 255]\)
char 16位
  Unicode 编码字符
U+0x0000 ~ U+0xFFFF
decimal 128位高精度数字值并且带28-29个有效位 (\([-7.9\times 10^{28}, 7.9\times 10^{28})] + 1 \cdots 28\)
double 64位双精度浮点数 \([\pm 5.0\times 10^{-324}, \pm 1.7\times 10^{308}]\)
float 32位单精度浮点数 \([\pm 1.5\times 10^{-45}, \pm 3.4\times 10^{38}]\)
int 32位带符号整数 \([-2147483648, 2147483647]\)
long 64位带符号整数 \([-9223372036854775808, 9223372036854775807]\)
sbyte 8位带符号整数 \([-128, 127]\)
short 16位带符号整数 \([-32768, 32767]\)
uint 32位无符号整数 \([0, 4294967295]\)
ulong 64位无符号整数 \([0, 18446744073709551615]\)
ushort 16位无符号整数 \([0, 65535]\)

数组(聚合类型)

数组是一个存储相同类型元素的固定大小的顺序集合。为什么会有数组呢?假设我们需要100个int变量,我们总不可能是真的给他们命名为 \(a_1, a_2, a_3 \cdots a_{100}\) 吧。数组的作用就是把他们都变成一个变量假设叫array,那么我们要访问第 \(i\) 个元素就可以很轻松的使用array[i]。原版的很多字段都是数组,比如玩家的背包就是一个Item数组。

注意:数组的下标从 \(0\) 开始,到 \(n-1\) 截止, \(n\) 就是数组的大小(总元素个数)

数组的声明方法有点不太一样:

<数据类型>[] <名字>;
// 生成一个叫numbers的float数组
float[] numbers;

但是这个数组是没有被初始化的,如果你直接使用它肯定会运行时报错。所以我们需要初始化一个初始大小:

// 生成一个叫numbers的float数组,初始化里面有100个元素
float[] number = new float[100];

给数组里面的变量赋值:

// 给第10个元素(数组下标9)赋值3.1415f
number[9] = 3.1415f;

获取数组里面的值也是一样直接在方括号里填入下标。注意下标别出界了(超出数组大小或者为负数),否则会报运行时错误IndexOutOfBoundException


Main.NewText函数

接下来我要介绍的这个Main.NewText函数是一个非常有用的函数,他能在屏幕输出一些信息。比如说当你挥动近战武器的时候告诉你玩家的位置:

public override bool UseItem(Player player) {
    Main.NewText($"玩家的坐标: X = {player.position.X}, Y = {player.position.Y}");
    return base.UseItem(player);
}

于是你就能看到

这个$"XXX{变量名}XX"语法其实是C#6以后才有的,它能直接把变量的值插入到字符串里面,于是你就能看到玩家的坐标被正确的显示出来了。

我们还可以改文字的颜色,具体关于函数的信息我们会在下一章去讲。如果你们对C#语法某个概念不理解不妨就把那个变量或者执行结果用Main.NewText打印在屏幕上吧。

Main.NewText($"玩家的坐标: X = {player.position.X}, Y = {player.position.Y}", Color.Red);

条件语句

条件表达式的值一定是bool类型,比如

// 大于
1 > 2
// 大于等于
1 >= 2
// 等于
1 == 2
// 自定义类型等于判定
a.Equals(b)
// 如果A本身就是bool类型
A

If语句

If语句有以下几种写法

if (<条件>) {
    /* 如果条件为true将执行的语句 */
}
if (<条件>) {
    /* 如果条件为true将执行的语句 */
}
else {
    /* 如果条件为false将执行的语句 */
}
if (<条件>) {
    /* 如果条件1为true将执行的语句 */
}
else if (<条件2>){
    /* 如果条件1为false但是条件2是true将执行的语句 */
}
else if (<条件3>){
    /* 如果条件1和2为false但是条件3是true将执行的语句 */
}
else {
    /* 如果条件123都是false将执行的语句 */
}

比如我们想让模板剑在白天黑夜、肉山前/后射出不同的弹幕,那么可以怎么写呢?

首先白天黑夜可以用Main.dayTime判定,具体哪个是白天黑夜我们可以用Main.NewText得知,也可以直接看名字得知。肉山前后用的是Main.hardMode这个字段。有了这些信息我们就可以开始写判定语句了。

public override bool Shoot(Player player, ref Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack) {
    if (!Main.hardMode && Main.dayTime) { // 肉山前,白天
        type = ProjectileID.LightBeam;
    } else if (!Main.hardMode && !Main.dayTime) { // 肉山前,夜晚 
        type = ProjectileID.NightBeam;
    } else if (Main.hardMode && Main.dayTime) { // 肉山后,白天 
        type = ProjectileID.SwordBeam;
    } else if (Main.hardMode && !Main.dayTime) { // 肉山后,夜晚 
        type = ProjectileID.TerraBeam;
    }
    return true;
}

!Main.dayTime的意思是把这个bool变量的值取反,也就是假的变成真的,真的变成假的,这句话的意思是当不是白天,也就是夜晚的时候,执行后续代码。进游戏测试一下效果

嗯,效果非常好。但是你知道吗,If语句也是可以嵌套的,我们可以先判是不是肉山后,然后再判定是不是白天:

if (Main.hardMode) {
    // 肉后的情况
    if (Main.dayTime) {
        type = ProjectileID.SwordBeam;
    } else {
        type = ProjectileID.TerraBeam;
    }
} else {
    // 肉前的情况
    if (Main.dayTime) {
        type = ProjectileID.LightBeam;
    } else {
        type = ProjectileID.NightBeam;
    }
}

这样看上去是不是简洁多了?但是我们还可以让它更简洁,需要用到一个叫做三元操作符的东西:

<条件> ? <条件为true的值> : <条件为false的值>

于是我们可以这么写,变得非常短

if (Main.hardMode) {
    type = Main.dayTime ? ProjectileID.SwordBeam : ProjectileID.TerraBeam;
} else {
    type = Main.dayTime ? ProjectileID.LightBeam : ProjectileID.NightBeam;
}

虽然可以把Main.hardMode也优化掉,但是三元操作符不能乱用,当条件多的时候,三元运算符会让这行代码变得非常长而且难以看懂。

当然我们还有一种条件语句是switch,因为用的少所以这里就不介绍了,大家就自己参考其他C#教程了呗。


循环语句

循环语句的作用就是防止重复,比如我要生成500个粒子,你难道真的写500行Dust.NewDust()?就算500你能写下,那\(1.14514 \times 10^9\)个呢?就算你能写下,硬盘也不一定装的下,就算你装得下,如果我不能预先知道要重复多少次呢?

while语句

while (<条件>) {
    // 满足条件时执行的语句
}

while语句会重复执行{}里面的内容,直到条件不满足,如果一开始条件就不满足,那么就不会执行任何一个语句。如果你的语句内容和条件无关或者无法让条件为false的话,那么这个循环语句可能会一直卡在这里,这种情况我们称为死循环,比如这就是一个标准的死循环:

while (true) {
    // 里面什么都没有
}

但是即使条件永远不会变成false,你也可以结束这个循环,那就是用break语句。

while (true) {
    // 强行跳出循环
    break;
}

比如我们想实现一个物品,当玩家背包里有模板剑才可以使用,那么我们怎么写呢?首先我们要知道如何访问玩家的背包,我可以告诉你们是player.inventroy,我们可以利用函数player.inventory.Length获取数组大小。

明确一下我们需要做的事情,首先把player.inventroy里面所有的Item都获取到,然后判定其中是否有item.type是模板剑的,只要我们找到一个就知道背包里有模板剑了,就可以直接退出循环。但是我们不能让下标出界,那么可以从0开始直到player.inventory.Length结束。这样我们就可以写代码了:

// 数组的下标
int i = 0;
// 这个变量表示有没有模板剑
bool hasSword = false;
// 如果下标小于数组大小
while (i < player.inventory.Length) {
    if (player.inventory[i].type == ModContent.ItemType<SkirtSword>()) {
        hasSword = true;
        // 发现模板剑提前结束循环
        break;
    }
    // 下标加一,准备获取下一个物品
    i++;
}

输出hasSword这个变量测试了一下效果不错。 注意那个i++,一元运算符++使用方法是变量名++;++变量名,意义是该变量自增1。如果变量名写在++后面,那么就返回增加后的值,否则返回增加前的值。

但是这么写好像有点长,有没有方法简化一下呢?

for语句

for ( <初始化> ; <条件> ; <每次循环结束后执行的语句> ){
    // 满足条件后执行的语句
}

初始化语句和每次循环结束后执行的语句都不是条件语句,而是单纯的语句。下面是 for 循环的控制流:

  1. <初始化> 会首先被执行,且只会执行一次。这一步可以声明并初始化任何循环控制变量。也可以不在这里写任何语句,只要有一个分号出现即可。
  2. 接下来,会判断 <条件>。如果为true,则执行循环主体。如果为false,则不执行循环主体,并且跳出这个for语句。
  3. 在执行完 for 循环主体后,控制流会跳回上面的 <每次循环结束后执行的语句>。该语句允许您更新循环控制变量。和初始化一样,语句可以留空,只要在条件后有一个分号出现即可。
  4. 条件再次被判断。如果为true,则执行循环,这个过程会不断重复。在条件变为false时,for 循环终止。

那么我们就可以把刚才的代码改成这样:

bool hasSword = false;
// 下标在这
for (int i = 0; i < player.inventory.Length; i++) {
    if (player.inventory[i].type == ModContent.ItemType<SkirtSword>()) {
        hasSword = true;
        // 发现模板剑提前结束循环
        break;
    }
}

那么还可不可以更简洁?(我是不是有短码强迫症)

foreach语句

foreach语句是C#的一个语法糖,什么叫语法糖呢?就是为了方便编写程序而设计的一种语法,foreach本质上可以用for循环代替(注意:反之不行)但是foreach比for更简单好写,但是遗憾的是,很多情况下foreach都用不了。

foreach (var <变量名> in <数组或者迭代器>) {
    // 执行循环语句
}

只有<数组或者可迭代的类型>才可以使用foreach语句,它会遍历数组中的每一个元素,然后你的<变量名>就是当前正在访问的元素的实例。

于是我们可以这么写:

bool hasSword = false;
foreach (var item in player.inventory) {
    if (item.type == ModContent.ItemType<SkirtSword>()) {
        hasSword = true;
        break;
    }
}

看上去是不是更加简单了呢?于是我们就完成判定玩家背包是否存在某个物品的代码了,我们可以用它完成饰品组合技的这个功能了!

这下你们能理解第一部分所讲的代码了吗?


练习

基础

  1. 假设我想让武器射出的弹幕伤害是原来的3.14159倍,那么应该如何改呢?答案
    // 注意强制转换
    damage = (int)(damage * 3.14159);
  2. 如何把字符串转化为int类型?double类型?答案
    // string转int
    int a = Convert.ToInt32("114514");
    // string转double
    double b = Convert.ToDouble("3.14159");
    double c = Convert.ToDouble("1.07e9");
    
    // 猜猜会发生什么
    int d = Convert.ToDouble("1.07e9");
    int e = Convert.ToInt32("2147483647114514");
  3. 假设A,B都是bool值,写一个if语句判断是否出现:A和B不同时为真或者同时为假。答案
    if ((A && !B) || (!A && B)) { }
    // 或者如果对位操作比较熟悉
    if (A ^ B) {}
  4. 假设A,B,C都是bool值,写一个if语句判断是否出现:ABC中存在两个值都为真。答案
    if ((A && B) || (A && C) || (B && C)) { }
  5. 写出这个循环语句最后A的值
    int A = 0;
    for(int i = 0; i < 100; i++) {
        A += i;
    }
    
    答案
    初始A是0,初始i是0,i最后会是99,每次循环i加1。于是我们有\[0+1+2 + \dots + 99 = \sum_{i=0}^{99} i = 4950\]
  6. 写出这个循环语句最后A的值
    int A = 0;
    for(int i = 0; i <= 100; i += 2) {
        A += i;
    }
    
    答案
    初始A是0,初始i是0,i最后会是100,每次循环i加2。于是我们有\[0+2+4 + \dots + 100 = \sum_{i=0}^{50} 2i = 2550\]
  7. 写一段代码,获取世界里所有NPC的生命值总和答案
    long sum = 0;
    foreach (var npc in Main.npc) {
        // 只有npc.active才是真实存在在世界的npc
        if (npc.active) sum += npc.lifeMax;
    }
    

进阶

  1. 这段代码的执行结果sum的值是?(如果你尝试运行了,祝你好运)
    long sum = 0;
    for (int i = 0; i < 1000; i++)
        for (int j = i; j < 1000; j++)
            for (int k = j; k < 1000; k++)
                for (int l = k; l < 1000; l++)
                    for (int m = l; m < 1000; m++) sum++;
    return sum;
    
  2. 这段代码的执行结果sum的值是?
    long sum = 0;
    for (int i = 0; i < 1000; i++)
        for (int j = 0; j < 1000; j++)
            for (int k = 0; k < 1000; k++)
                for (int l = 0; l < 1000; l++)
                    for (int m = 0; m < 1000; m++) sum++;
    return sum;
  3. 如何初始化多维数组?初始化引用类型的数组会有什么问题?
  4. 查询一下运算优先级表,这段代码会输出什么?35 & 324 % 3 + 35 * 26 / 2 % 100
  5. 查一下关于List的资料,与数组相比,List有哪些优缺点?
  6. 泰拉瑞亚原版里世界物品,NPC,弹幕,物块数组都有多大?

《C#基础知识(1)》有16个想法

  1. Pingback: 第2章 使用变量、操作符和表达式 - 裙中世界

发表回复