欢迎来到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
, short
和long
等等类型,他们代表的不同的位数。
浮点型(小数)
浮点型,就是带着小数的数字,如-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,而是一个引用类型的实例,所以我们不能给它一个数字。
注意,不同自定义数据类型之间不能直接赋值,除非定义了转化的规则。但是某些基本数据类型之间是可以隐式转换的,比如float
转double
。
这里有一张基本数据类型的表,我就不多做讲解了,有兴趣的同学自己研究一下吧。
类型 | 代表含义 | 取值范围 |
---|---|---|
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 循环的控制流:
- <初始化> 会首先被执行,且只会执行一次。这一步可以声明并初始化任何循环控制变量。也可以不在这里写任何语句,只要有一个分号出现即可。
- 接下来,会判断 <条件>。如果为true,则执行循环主体。如果为false,则不执行循环主体,并且跳出这个for语句。
- 在执行完 for 循环主体后,控制流会跳回上面的 <每次循环结束后执行的语句>。该语句允许您更新循环控制变量。和初始化一样,语句可以留空,只要在条件后有一个分号出现即可。
- 条件再次被判断。如果为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; } }
看上去是不是更加简单了呢?于是我们就完成判定玩家背包是否存在某个物品的代码了,我们可以用它完成饰品组合技的这个功能了!
这下你们能理解第一部分所讲的代码了吗?
练习
基础
- 假设我想让武器射出的弹幕伤害是原来的3.14159倍,那么应该如何改呢?答案
// 注意强制转换 damage = (int)(damage * 3.14159);
- 如何把字符串转化为
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");
- 假设A,B都是
bool
值,写一个if语句判断是否出现:A和B不同时为真或者同时为假。答案if ((A && !B) || (!A && B)) { }
// 或者如果对位操作比较熟悉
if (A ^ B) {} - 假设A,B,C都是
bool
值,写一个if语句判断是否出现:ABC中存在两个值都为真。答案if ((A && B) || (A && C) || (B && C)) { }
- 写出这个循环语句最后
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\] - 写出这个循环语句最后
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\] - 写一段代码,获取世界里所有NPC的生命值总和答案
long sum = 0; foreach (var npc in Main.npc) { // 只有npc.active才是真实存在在世界的npc if (npc.active) sum += npc.lifeMax; }
进阶
- 这段代码的执行结果
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;
- 这段代码的执行结果
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;
- 如何初始化多维数组?初始化引用类型的数组会有什么问题?
- 查询一下运算优先级表,这段代码会输出什么?
35 & 324 % 3 + 35 * 26 / 2 % 100
- 查一下关于List的资料,与数组相比,List有哪些优缺点?
- 泰拉瑞亚原版里世界物品,NPC,弹幕,物块数组都有多大?
学会了
沙发,来了
我来了!学会了!
前排支持~
114514太臭了【】
啥是三精度浮点数?(手动滑稽)
C++ long double
Float96
留名
完了,没听懂,怎么办
可以去网上找C#教程看一看,这个只是作为C#教程的补充
感觉我好笨啊进阶到后面就没做出来过
捉虫:float是32位单精度浮点数,你写成64位了
已改
多谢
Pingback: 第2章 使用变量、操作符和表达式 - 裙中世界