教程二 · 第 2 章:运算符、流程控制、方法
2.1 运算符
算术
int a = 10, b = 3;GD.Print(a + b); // 13GD.Print(a / b); // 3!整数除整数还是整数(截断)GD.Print(a % b); // 1,取余:判断整除、循环索引超有用GD.Print(10f / 3f);// 3.333...,想要小数结果至少一边是 float/double
a++; // 等同 a = a + 1a += 5; // 等同 a = a + 5,-= *= /= 同理GD.Print(2 + 3 * 4); // 14,先乘除后加减,拿不准就加括号Godot 场景:冷却归零、每 N 波刷怪:
_attackCooldown -= dt;if (_enemiesKilled % 5 == 0) GD.Print("每杀 5 个触发一次");比较与逻辑
bool isLow = hp < 30;bool canAct = hp > 0 && isGrounded; // && 与:两边都真bool hit = wasAttacked || steppedTrap; // || 或:任一为真bool notGrounded = !isGrounded; // ! 非
if (hp >= 0 && hp <= 100) { } // 区间判断要写两遍,不能 0 <= hp <= 100短路特性:a && b,a 为假时 b 根本不计算。利用它防 null:
if (_target != null && _target.IsInsideTree()) { } // 安全:先确认存在三元运算符:二选一的简写
string stateName = isGrounded ? "地面" : "空中";// 等价于// string stateName;// if (isGrounded) stateName = "地面"; else stateName = "空中";Velocity = Velocity with { X = dir != 0 ? dir * speed : Velocity.X };别嵌套三元,难读。
位运算(了解即可)
Godot 碰撞层用 32 位掩码,会看到这些:
SetCollisionLayerValue(2, true); // 打开第 2 层uint mask = CollisionMask;bool hitsLayer3 = (mask & (1u << 2)) != 0; // 检查第 3 位<< 移位、& 与、| 或、^ 异或。现在眼熟即可,直接用 SetCollisionLayerValue 更不容易错。
2.2 if / else
public void TakeDamage(int amount){ if (_invincible) return; // 提前返回,少写 else 嵌套
_hp -= amount;
if (_hp <= 0) { _hp = 0; Die(); } else if (amount >= 30) { PlayHurtAnim("heavy"); } else { PlayHurtAnim("light"); }}- 条件必须是
bool:C# 不像 C/Python,if (hp)直接编译错误,要写if (hp > 0) - 单行可以不写花括号,但新手建议永远写花括号,以后加语句不会踩坑
- 提前 return / continue 降低嵌套,是写清楚逻辑的第一技巧
模式匹配:现代 C# 的 if 超能力
教程一里球撞挡板写过 if (collider is Paddle paddle):
Node collider = collisionInfo.GetCollider();
if (collider is Paddle paddle) // 类型检查 + 转换一步完成{ BounceFromPaddle(paddle); // paddle 已当作 Paddle 类型可用}else if (collider is StaticBody2D){ _direction.Y = -_direction.Y;}老写法(要记住,看别人代码会遇到):
if (collider is Paddle){ var paddle = (Paddle)collider; // 先判断再强转,啰嗦}is 还能配合属性:
if (collider is Enemy { Hp: <= 0 }) return; // 是敌人且血量已为 0switch 表达式(多分支算一个值时比 if-else 干净):
float damageMult = element switch{ DamageType.Physical => 1.0f, DamageType.Fire => 1.5f, DamageType.Ice => 0.8f, _ => 1.0f // _ 是"其他所有情况"};2.3 循环
while:先看条件
int bullets = 3;while (bullets > 0){ FireOneShot(); bullets--;}for:次数明确
for (int i = 0; i < 5; i++){ GD.Print($"第 {i} 发"); // 0,1,2,3,4}结构:for(初始化; 继续条件; 每轮收尾)。倒着来:for (int i = list.Count - 1; i >= 0; i--)——边遍历边删除时必须倒序(第 6 章讲)。
foreach:遍历一切”可枚举”
foreach (Node child in GetChildren()){ child.QueueFree();}
foreach (var node in GetTree().GetNodesInGroup("enemy")){ if (node is Enemy enemy) enemy.TakeDamage(10);}最常用、最不容易出错的循环。限制:遍历过程中不能直接增删集合元素(会抛 InvalidOperationException)。
跳出控制
break; // 立刻结束整个循环continue; // 跳过本轮剩余语句,进入下一轮MoveAndSlideCollide 的碰撞遍历你在 Pong 里见过:
var collisions = MoveAndSlideCollide();for (int i = 0; i < collisions.Count; i++){ var c = collisions[i]; // ...}2.4 方法(函数)
方法是打包一段逻辑、起个名字、可传参、可返回结果的机制。
// 返回类型 方法名(参数列表)private int CalcDamage(int baseAtk, float critRate){ if (GD.Randf() < critRate) return baseAtk * 2; // return 把结果送回调用处并立刻结束方法 return baseAtk;}
private void Die() // void:不返回任何东西{ EmitSignal(SignalName.Died); QueueFree();}调用:
int dmg = CalcDamage(20, 0.25f);参数传递的几个要点
值参数是副本(值类型传进去,外面不变):
void AddOne(int x) { x++; }AddOne(hp); // hp 本身不变引用类型传的是”遥控器”(节点、自定义类传进去,改成员等于改本体):
void Kill(Player p) { p.Hp = 0; } // 外面那个 Player 真的被改out / ref(需要”多个返回值”时):
if (PhysicsIntersectPoint(point, out var enemy)){ enemy.TakeDamage(10); // Godot 很多 TryGet 风格 API 都是 out 参数}TryXxx(参数, out 结果) 返回 bool 表示成不成功、结果走 out,是 C# 生态的通用惯例。Godot 的射线检测、字典取值都会见到。
方法重载:同名不同参数
public void Heal(int amount) { _hp += amount; }public void Heal(float percent) { _hp += (int)(MaxHp * percent); } // 同名不同参,合法调用处按参数类型自动匹配。Godot API 大量重载,例如 DrawCircle / DrawArc 参数个数不同的版本。
默认参数与命名参数
private void SpawnEnemy(string type = "slime", int count = 1, float delay = 0f)
SpawnEnemy(); // slime, 1, 0SpawnEnemy("bat", 3);SpawnEnemy(count: 5, delay: 2f); // 命名参数可跳过默认值,顺序随意方法拆多大合适
经验:一个方法一个意图,超过一屏(约 40 行)就该想办法拆。Godot 脚本里典型结构:
public override void _PhysicsProcess(double delta){ ApplyGravity(delta); HandleInput(); UpdateStateMachine(); MoveAndSlide();}_PhysicsProcess 只做编排,每步一个名字清楚的私有方法——读代码像读目录。
2.5 局部函数
方法里还能定义临时方法(复杂计算拆步又不想污染类时用,少见但要认识):
public int Combo(int a, int b){ int Square(int x) => x * x; // 表达式体方法 return Square(a) + Square(b);}=> 叫表达式体成员,单行方法的简写:
public bool IsDead => _hp <= 0; // 属性的表达式体,第 4 章讲private void Stop() => Velocity = Vector2.Zero;2.6 静态:属于类而不是某个对象
GD.Print、Mathf.Clamp、Input.IsActionPressed——注意这些调用不需要先 new 对象:
GD.Print("x"); // 静态方法:类名.方法名Mathf.Clamp(y, 0f, 540f);Input.GetAxis("left", "right");static 成员挂在类上、全程序共享一份。自己写工具方法也可以:
public static class GameMath{ public static float Knockback(float strength, float mass) => strength / mass * 10f;}// 调用:GameMath.Knockback(50, 2);⚠️ Godot 节点生命周期由引擎管理,不要自己 new Node 派生类当工具(new Player() 不在场景树里,什么都不会发生)。静态工具类只放纯计算。
2.7 一个综合例子:平台跳跃输入处理
把本章东西串起来(对照教程一第 4 章代码重读一遍,现在每行都该认识):
public override void _PhysicsProcess(double delta){ float dt = (float)delta;
if (!IsOnFloor()) Velocity = Velocity with { Y = Velocity.Y + Gravity * dt };
float dir = Input.GetAxis("move_left", "move_right"); Velocity = Velocity with { X = dir * MoveSpeed };
if (IsOnFloor()) _coyote = CoyoteTime; else _coyote -= dt;
if (Input.IsActionJustPressed("jump")) _buffer = JumpBufferTime; else _buffer -= dt;
if (_buffer > 0f && _coyote > 0f) { Velocity = Velocity with { Y = JumpVelocity }; _buffer = 0f; _coyote = 0f; }
MoveAndSlide();}新的没有了——游戏代码 80% 是”取值、判断、改字段、调引擎方法”的排列组合。语言部分到本章其实已经能读大部分游戏脚本,剩下的核心是面向对象(第 4–5 章)。
2.8 作业
- 写方法
ClampHp(int hp):用 if-else 把任意整数夹在 0–100 返回;再用Mathf.Clamp重写一遍(体会库函数的意义)。 - 写
GetScoreGrade(int score)用 switch 表达式返回 “S/A/B/C”(自己定分段)。 - 用 foreach 遍历
GetChildren(),打印每个子节点的名字和类型(child.GetType().Name)。 - 实现一个方法
BounceOffCeiling():只在 Y 速度向下时反转 Y 速度(注意 Godot 里 y 向下为正,想想条件该怎么写)。
第 3 章:数组、字符串、空值、异常——处理”一串数据”和”出错怎么办”。
《Godot C# 零基础》系列导航
⬅️ 上一篇:为 Godot 学 C# 01:变量与类型 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:为 Godot 学 C# 03:数组、字符串、空值与异常