Unity游戏开发手札:游戏开发基石——C#编程基础篇(1)

4,554 阅读20分钟

写在前面

欢迎来到WunderMinibar新的栏目——Unity游戏开发手札。在这个栏目里我们将与大家一起探索Unity游戏开发的世界,分享在学习过程中遇到的问题和心得,也希望能给想要学习Unity开的小白一些帮助。我们团队的两位学技术的同志本身也是初学者,我们编写这个栏目,希望通过记录我们学习过程,与大家共同成长和交流。

首先,让我们从C#基础知识开始。C#是Unity游戏开发的主要编程语言,掌握C#基础对于我们利用Unity游戏引擎成功开发出游戏来讲至关重要。为了让方便大家阅读,我们会尽量用简洁且通俗易懂的语言,将枯燥的变成内容变得有趣一些。现在,让我为大家介绍C#的基本语法、数据类型、控制结构等概念。

变量

在正式开始介绍关于C#编程语言中的“变量”和“常量”之前,我先给大家讲一下“变量”和“常量”在Unity开发中的用处和重要性。

在游戏开发中,我们可以使用“变量”和”常量“来表示游戏对象(任何游戏中的物体或者人物都可以称作游戏对象)的属性(properties)和状态(status)。属性是用来描述游戏对象的特征,例如颜色、大小、材质、形状等。而状态指的是游戏对象当前所处的条件和行为,例如站立、跑动、跳跃等。

假设我们正在开发一款2D横板游戏,游戏存在一个主角和一个怪物,那我们就需要给这两个游戏对象设置属性与状态,代码如下:

//游戏对象的属性
int maxHealth = 100;          //血量值
int currentHealth;                 //当前血量
float runSpeed = 10.5f;       //跑步速度
float walkSpeed = 5.5f;       //行走速度
float jumpForce = 16.5f;      //弹跳力


//游戏对象的状态
bool isGround;                //判断游戏对象是否在地面
bool isHurt;            //判断游戏对象是否受到伤害
bool isDead;                //判断游戏对象是否死亡

通过以上代码,我们用了几行代码设置了游戏对象的属性和状态,在游戏过程中,我们可以根据玩家的操作和游戏难度和规则修改这些变量的值,从而实现游戏的各种交互功能和调节游戏的平衡性。例如,当怪物受到觉得攻击时,我们可以减少其生命值;当主角跳起来时候,我们可以改变isGround的状态。

读到这里,想必你肯定会有很多疑惑,例如:什么是int float bool10.5f中的f的又是什么,接下来的内容,我就将详细地介绍“变量”和”常量“。结合下面的C#的内容和上面这段例子,或许你可以对C#的变量和如何在游戏中运用有一定初步的了解。

代码格式

  1. 变量:【类型】【名称】;
  2. 常量:const【类型】【名称】;
// 变量与常量
int myInt = 10;
const int myConstInt = 20;

变量与常量

当我们编写游戏或任何类型的软件时,我们需要一种方式来保存和管理我们的数据。这就是变量和常量的用途。

变量:你可以把变量想象成一个可变的数据容器。你可以把数据放进去,也可以把数据取出来,甚至可以完全改变里面的数据。这就像一个储物箱,你可以放进各种各样的物品,也可以随时取出来。

常量:与变量不同,常量是一个一旦被赋值就不能改变的数据容器。这就像一个密封的储物箱,一旦你把东西放进去并封上,就不能再更改里面的东西了。

类型

【整数】sbyte、sbort、int、long、byte、usbort、uint、ulong

【小数】float、duoble

【文字】char(单个文字,单引号包裹)

【逻辑】bool(true / false)

基本类型

C#数据的基本类型包含整数、小数、文字、逻辑四个大类。不同的整数类型拥有不同的容量和对负数的支持(比如long比int大,uint不支持负数等),不同的小数类型拥有不同的精确值(double 比float精度高),小数也叫浮点数,这和小数在内存中的表示方式有关。逻辑值也叫布尔值,即bool的音译,来自数学家George Boole(1815-1864)这四个基本数据类型大类在C#编程中扮演着重要角色,掌握它们对于游戏开发至关重要。

// 基本类型
float myFloat = 3.14f;
char myChar = 'A';
bool myBool = true;

关于名称

请赋予代码清晰的标识!

在编程中,我们给变量、函数和其他代码元素赋予名称,这有助于我们理解和跟踪它们的作用。然而,我们不能随意给它们命名,因为C#和大多数其他编程语言有一定的命名规则:

  • 名称可以由字母、数字和下划线(_)组成。
  • 名称不能以数字开头。例如,"1stPlayer" 是一个无效的名称,但 "firstPlayer" 或 "player1" 是有效的。
  • 名称不能与C#的内置关键词重复。例如,你不能使用 "int" 或 "for" 作为名称,因为它们是C#的关键词。
  • 你应该尽量使用有意义的名称。这意味着名称应能描述其代表的数据或操作。例如,如果你有一个变量用于存储玩家的生命值,那么 "playerHealth" 就是一个比 "ph" 或 "x" 更好的名称。

遵循这些规则可以帮助你编写出更清晰、更易于理解的代码。毕竟,好的代码就像一篇好的故事,它应该能清楚地传达其含义,而不需要过多的解释。

声明一个量

规定量的基本类型的操作我们称之为声明一个量。其基本格式为类型词缀 + 名称 + 分号,常量则在前面加上const词缀。声明和赋值可以在同一行代码中实现(见代码第6-8行)。你也可以通过逗号分隔名称的方式一次声明多个同类型量。

不过请注意:float类型对应的数值,需要在后面加上f,比如0.1f;char类型对用的数值,需要用单引号包裹,比如 'a'

// 变量声明
int age;       // 声明一个整数变量,名为age
float height;  // 声明一个浮点数变量,名为height
char 'a','b','c','d';  // 声明多个变量

// 变量赋值 
age = 18;      // 给age变量赋值18
height = 1.75f; // 给height变量赋值1.75

// 常量声明并赋值
const int maxScore = 100; // 声明一个整数常量,名为maxScore,值为100

类型

量的尺寸

在编程中,不同的数据类型会占用不同的内存空间。例如,8位的 byte 类型可以表示 2^8(即256)个不同的数值,范围从 0 到 255。虽然 bool 类型只需要1位就能区分真假,但在大多数编程语言中,包括C#,bool 类型的尺寸仍然为 8 位(即1字节)。

类型转换

程序中经常需要进行数据类型的转换。这种转换可以是隐式的,也可以是显式的。

  • 隐式转换:这种转换在不丢失信息的情况下自动进行,比如从小的数据类型(如 int)转换到大的数据类型(如 long)。注意,字符类型 char 在这种转换中也会被视为一个 2 字节的整数。
  • 显式转换:当需要将大的数据类型转换为小的数据类型时,我们必须使用显式转换。这是因为这种转换可能会丢失信息,所以编程语言不会自动进行这种转换。例如,将一个 int 类型的值转换为 float 类型的值,就需要使用显式转换:
// 类型转换
int intValue = 42;
float floatValue = (float)intValue;

这段代码中,(float) 是一个转换运算符,它将 intValue 转换为 float 类型。注意,逻辑类型 bool 不参与这种转换,但可以通过其他方法转换。

运算符

算术运算符

算术运算符:【+】【-】【*】【/】【++】【--】

++:用于某个变量的自增,每执行一次++代表着这个变量+1

++:用于某个变量的自减,没执行一次--代表着这个变量-1

// 算术运算符的简单运用
int a = 5;
int b = 3;
int sum = a + b; // sum为8
int diff = a - b; // diff为2
int prod = a * b; // prod为15
int quot = a / b; // quot为1(整数除法)

算数运算符是对数字进行计算的运算符,包含加减乘除、取余和自增自减,运算中数据会统一转换成其中取值范围最大的类型,整数除以整数会丢失小数精度,小数可以对小数取余,自增自减分为先增减和后增减,不要使用过于复杂的自增自减。

在上文 “变量” 中给大家提到了关于玩家血量、移动速度等的赋值。那在算数运算符这里,我再给大家举一个角色驾驶游戏的例子。以便于大家来理解算数运算符在Unity游戏开发中的运用。

假设我们正在创建一个角色驾驶游戏。在这个简单的小游戏中,玩家需要通过操控一辆车来通过一系列障碍、收集道具以及避免障碍撞击。

在这个环境中我们可以如下应用算数运算符:

// 算术运算符
int playerSpeed = 5; //玩家的速度
int obstacleDamage = 3; //障碍物的伤害
int collectedItem = 1; //收集到的道具数量

// 假设玩家加速了
playerSpeed = playerSpeed + 2; // "+" 运算符使速度增加。现在playerSpeed是7。

// 玩家撞到了障碍物
playerSpeed = playerSpeed - obstacleDamage; // "-" 运算符使速度减少。现在playerSpeed是4。

// 玩家收集了一个加速道具
playerSpeed = playerSpeed * 2; // "*" 运算符使速度加倍。现在playerSpeed是8。

// 玩家的加速道具效果结束了,速度恢复正常
playerSpeed = playerSpeed / 2; // "/" 运算符使速度减半。现在playerSpeed是4。

// 玩家又收集到了一个道具
collectedItem++; // "++" 运算符使道具数量增加。现在collectedItem是2。

// 玩家使用了一个道具
collectedItem--; // "--" 运算符使道具数量减少。现在collectedItem是1。

关系运算符

关系运算符:【==】【!=】【>】【<】【>=】【<=】

比较数字大小关系的运算符,包含相等、不等、大于、小于、大于等于、小于等于,运算结果为逻辑值true或false,小数在计算机中的存储方式特殊,精度无法保证,因此 == 和 != 直接用小数比较时可能会出现问题

// 关系运算符的简单运用
bool isEqual = a == b; // isEqual为false
bool isGreater = a > b; // isGreater为true

Unity游戏开发中,关系运算符就像是我们的视觉和触觉一样重要。它帮助我们判断游戏的各种情况,比如玩家是否还有生命力,或者玩家是否已经得到足够的分数来解锁新的级别。

下面我再举一个生动的例子来帮助大家理解关系运算符的作用。假设我们在开发一款关于射击游戏,在这个游戏中,玩家需要与一波又一波的怪物战斗,消灭他们并积累分数。

// 关系运算符
int playerHealth = 10; //玩家的生命值
int playerScore = 0; //玩家的分数
int scoreToWin = 100; //玩家需要达到的分数才能赢得游戏

// 想象一下,玩家在激烈的战斗中受到了伤害
playerHealth -= 5; // 玩家生命值减少
if (playerHealth <= 0) // 使用 "<=" 运算符检查玩家是否还有生命值
{
    // 如果玩家的生命值小于等于0,那么游戏结束
    Debug.Log("游戏结束了,再接再厉!");
}

// 好消息!玩家成功地消灭了一只怪物并获得了一些分数
playerScore += 20; 
if (playerScore >= scoreToWin) // 使用 ">=" 运算符检查玩家的分数是否已经达到了胜利的条件
{
    // 如果玩家的分数大于等于胜利条件,那么玩家赢得了游戏
    Debug.Log("恭喜你,你赢了!接下来的挑战更加刺激!");
}

// 看,场上出现了一个特殊的道具,只有当玩家的分数等于50时才能使用
if (playerScore == 50) // 使用 "==" 运算符检查玩家的分数是否等于50
{
    // 如果玩家的分数等于50,那么玩家可以使用这个特殊的道具
    Debug.Log("赶快行动,特殊道具等你来拿!");
}

如果你是纯变成小白,那么上面的代码可能你会有些看不懂的点。不要担心,下面的内容和知识点将会解答你的疑惑。

逻辑运算符

逻辑运算符:【&&】【||】【!】

  1. &&:这是逻辑"与"运算符。如果两边的表达式都为真,那么整个表达式就为真。
  2. 假设你正在制作一个角色需要满足两个条件才能使用特殊技能的游戏:他们的能量(energy)必须满足一定的阈值,并且他们必须拿到一个特殊物品(hasSpecialItem)。你可能会这样写代码:
if (energy > 50 && hasSpecialItem) {
    // 使用技能
}
  1. ||:这是逻辑"或"运算符。如果两边的表达式中有一个为真,那么整个表达式就为真。
  2. 现在,你想修改规则,只要满足两个条件之一就能使用技能。你可以这样修改代码:
if (energy > 50 || hasSpecialItem) {
    // 使用技能
}
  1. !:这是逻辑"非"运算符。它会把表达式的结果反过来。如果表达式为真,那么结果就为假,反之亦然。
  2. 有时,你想检查一个条件是否不满足。例如,如果角色没有装备武器(!hasWeapon),他们不能攻击。你可能会这样写代码:
if (!hasWeapon) {
    // 显示错误消息
}

赋值运算符:【=】【+=】【-=】【*=】【/=】【%=】

  1. =:这是赋值运算符。它把右边的值赋给左边的变量。
  2. +=, -=, *=, /=, %=:这些是复合赋值运算符。它们是赋值和其他运算(加、减、乘、除、求余)的组合。例如,a += b等价于a = a + b
  3. 这些运算符常常用来更新游戏状态。例如,每次角色收到伤害,你可能需要减少他们的生命值(health):
health -= damage;
  1. 当角色获得经验值(experience)时,你可能会使用+=运算符来增加他们的总经验值:
totalExperience += experience;

位运算符:【&】【|】【^】【~】【<<】【>>】

  1. &:这是位"与"运算符。它对两个数字进行位级别的运算。只有当两个对应的位都为1时,结果位才为1。
  2. |:这是位"或"运算符。它也是对两个数字进行位级别的运算。只要有一个对应的位为1,结果位就为1。
  3. ^:这是位"异或"运算符。如果两个对应的位一个为0,一个为1,结果位才为1。
  4. ~:这是位"非"运算符。它会反转数字的所有位。
  5. <<, >>:这是位移运算符。<<是左移运算符,会把数字的所有位向左移动指定的位数。>>是右移运算符,会把数字的所有位向右移动指定的位数。
  6. 在游戏开发中,位运算符可能不如逻辑和赋值运算符常见,但在某些情况下,它们可以用来优化代码。例如,假设你有一个表示角色状态的整数变量characterState,你可以使用位运算符来设置和检查特定的状态位:
// 设置角色为无敌状态(第1位)
characterState |= (1 << 0);

// 检查角色是否处于无敌状态
bool isInvincible = (characterState & (1 << 0)) != 0;

// 取消角色的无敌状态
characterState &= ~(1 << 0);
  1. 上述代码中,我们使用了位或(|)操作来设置特定的状态位,使用位与(&)操作来检查特定的状态位是否被设置,使用位非(~)操作来翻转特定的状态位,最后使用位移(<<>>)操作来选择特定的状态位。
    1. 在游戏的某些特定系统(如图形渲染、网络编程、数据压缩等)中,你可能会更常见的使用到位运算符。但是,如果你是初学者,那么你可能不需要太担心这部分,因为在大多数常见的游戏开发任务中,逻辑运算符和赋值运算符会更常用。

控制流

控制流

在编程中,控制流是一种非常重要的概念,它决定了代码执行的顺序。默认情况下,计算机会从代码的第一行开始,按照顺序一行一行地执行代码。但有时候,我们可能需要改变这个顺序,例如在某些条件下跳过一部分代码,或者重复执行一部分代码。这就需要用到控制流。对于控制流,我们主要有两种结构:条件分支循环结构

条件分支

条件分支有两种主要形式:if-elseswitch-case

  1. if-else:你可以把 if-else 结构看作是一个问题的答案。if 后面的部分是一个问题,如果答案是"真",那么就执行 if 后面的代码块;如果答案是"假",那么就执行 else 后面的代码块。比如在一个冒险游戏中,你的角色遇到了一个分岔路口,如果路口有怪物,你就选择逃跑(if 有怪物,else 逃跑)。
  2. switch-caseswitch-case 结构可以看作是一个多选题。switch 后面的部分是一个问题,然后你可以列出多个可能的答案(case),每个答案对应一个代码块。如果问题的答案和某个 case 匹配,那么就执行这个 case 对应的代码块。比如在一个冒险游戏中,你的角色遇到了一个宝箱,宝箱里可以是金币、宝石或者装备,你可以用 switch-case 结构来处理这个情况(switch 宝箱,case 金币,case 宝石,case 装备)。

if-else 结构中,我们还可以使用 else if 来处理多个条件。而在 switch-case 结构中,如果某个 case 后面没有 break,那么满足这个 case 的情况下,程序会继续执行下一个 case 的代码,这被称为"穿透"。

下面我们举一个Unity开发冒险游戏的例子,玩家角色可能遇到各种情况,我们可以使用条件分支来决定玩家的行为。

  1. if-else 处理遇到敌人的情况
bool enemyInSight = true;  // 假设我们可以检测到是否有敌人在视线内

if (enemyInSight)
{
    Debug.Log("敌人在附近!准备战斗!");
    // 这里可以放置战斗的代码
}
else
{
    Debug.Log("附近没有敌人。继续探索。");
    // 这里可以放置探索的代码
}
  1. switch-case处理打开宝箱的情况
string chestContent = "gold"; // 假设我们已经知道宝箱里面是金币

switch (chestContent)
{
    case "gold":
        Debug.Log("你找到了一些金币!");
        // 这里可以放置获取金币的代码
        break;

    case "gem":
        Debug.Log("你找到了一颗宝石!");
        // 这里可以放置获取宝石的代码
        break;

    case "equipment":
        Debug.Log("你找到了一件装备!");
        // 这里可以放置获取装备的代码
        break;

    default:
        Debug.Log("宝箱是空的。");
        // 这里可以放置处理空宝箱的代码
        break;
}
  1. 在这个例子中,根据chestContent的值,我们执行不同的代码:如果是"gold",则获取金币;如果是"gem",则获取宝石;如果是"equipment",则获取装备;如果是其他值,那么我们就认为宝箱是空的。

循环结构

循环结构分为while和for两种类型,while会在条件满足时重复执行内容,而do-while会先执行do后内容,再判断while,即无论满足条件至少执行一次。for循环需要定义计数器,设定计数器范围,并让计数器走动,break会跳出当前循环,continue会跳过当次循环。

while和for在Unity游戏开发用的非常的多,也是编程语言非常常见的一个结构,让我们一步步解析这些循环结构。

你可以把循环看作是一中重复执行某些操作的方法。它就像是一个音乐播放器的“重复播放”功能,只不过在这里,我们不是重复播放歌曲,而是重复执行一段代码。

  1. while 循环:你可以将 while 循环看作是一种条件循环。它会一直重复执行某个操作,直到不再满足某个条件。比如你正在玩一个游戏,你的角色在一个房间里不断行走,直到找到出口。这就是一个 while 循环的例子:while(没有找到出口){继续行走}
  2. do-while 循环do-while 循环和 while 循环很像,只是它们的执行顺序不同。do-while 循环会先执行一次操作,然后再检查条件。即使条件一开始就不满足,它也会至少执行一次操作。比如你在玩一个游戏,你的角色在每个房间都至少看一次,无论房间里是否有你要找的东西。这就是一个 do-while 循环的例子:do{看一眼房间} while(房间里没有找到东西)
  3. for 循环for 循环是一种计数循环。它会重复执行某个操作一定的次数。比如你在玩一个游戏,你的角色要跳过10个障碍。这就是一个 for 循环的例子:for(i=0; i<10; i++){跳过一个障碍}

breakcontinue 是循环中的特殊命令。break 命令可以提前结束循环,就像在音乐播放器中点击"停止"按钮一样。而 continue 命令则是跳过循环中的当前迭代,直接进入下一次迭代,就像在音乐播放器中点击"下一首"按钮一样。

我们举一个whiledo-whilefor循环在游戏开发中的具体应用,以玩家收集金币为例,假设玩家每秒收集一枚金币,我们要计算玩家需要多少秒才能收集到10枚金币。

  1. while循环版本:
int coinsCollected = 0; // 初始化金币数量为0
int secondsPassed = 0; // 初始化已经过的秒数为0

while (coinsCollected < 10) // 当金币数量小于10时,继续循环
{
    coinsCollected++; // 每秒收集一枚金币
    secondsPassed++; // 时间流逝
}
Debug.Log("玩家需要 " + secondsPassed + " 秒才能收集10枚金币.");
  1. do-while循环版本:
int coinsCollected = 0; // 初始化金币数量为0
int secondsPassed = 0; // 初始化已经过的秒数为0

do 
{
    coinsCollected++; // 每秒收集一枚金币
    secondsPassed++; // 时间流逝
} while (coinsCollected < 10); // 当金币数量小于10时,继续循环

Debug.Log("玩家需要 " + secondsPassed + " 秒才能收集10枚金币.");
  1. for循环版本:
int secondsPassed; // 已经过的秒数

for (secondsPassed = 0; secondsPassed < 10; secondsPassed++) // 每次循环,已过的秒数加1
{
    // 在这里,每次循环都相当于收集一枚金币,所以不需要额外的收集金币的操作
}
Debug.Log("玩家需要 " + secondsPassed + " 秒才能收集10枚金币.");

在这三个例子中,我们都在用不同的循环结构达成相同的目的:计算玩家需要多少秒才能收集到10枚金币。这三种循环结构各有优劣,选择哪种取决于具体的应用场景和个人偏好。

Concluding Remarks

非常感谢你能看到这里,我尽力用通俗的语言将Unity游戏开发的基础概念分享给大家,包括变量、数据类型、运算符,以及控制流。

我们首先探讨了变量和数据类型,它们是构建我们的游戏世界的基本砖石。接着,我们讨论了运算符,它们是我们处理游戏数据的工具,帮助我们进行算术运算,做出比较,以及处理逻辑判断。最后,我们深入了解了控制流,它是我们规划游戏逻辑的地图,帮助我们决定哪部分代码应该何时执行。

每个示例都是为了让你更好地理解这些概念,并鼓励你尝试在自己的Unity项目中应用它们。记住,掌握这些基础概念是你成为一名出色的游戏开发者的关键。当你对这些基础有了深入的理解和实践经验,你就可以开始创造自己的复杂游戏世界了。

我希望这篇文章能帮助你开始Unity游戏开发的旅程,并在你的编程道路上提供指导。任何大型游戏都是从简单的基础概念开始构建的。现在,拿起键盘,开始你的游戏开发之旅吧。

欢迎关注WunderMinibar公众号:WunderMinibar工作室