在"乡村巡游者"关卡里,英雄要在村庄的几个入口之间来回巡逻、清剿敌人。关卡一开始就点出了痛点:"在村庄中巡逻是很无聊的重复工作。" 每到一个巡逻点,你都要重复写"找敌人 → 判断 → 攻击"这同样的几行代码——点越多,重复得越多,代码又长又乱。
有没有办法只写一次这套逻辑,然后到处反复使用?有!这就是本单元的主角:函数(Function)。关卡说得很直白:"使用函数可以让重复工作轻松许多。你可以将很多行代码放到一行里!"
函数为什么这么重要?因为它一次性带来了三个好处:
| 好处 | 含义 | 带来的改变 |
|---|---|---|
| 浓缩代码 | 把很多行代码压缩成一行调用 | 主循环变得短小清爽 |
| 避免重复 | 同一段逻辑只写一次,到处复用 | 不用反复键入相同代码 |
| 提升可读性 | 用有意义的名字代表一整套逻辑 | 复杂逻辑变得一目了然 |
关卡里有一句很形象的话:"这是函数的第一大好处:它们能将大量代码浓缩到一行。" 想象一下,如果每次挥剑都要写 25 行代码,那将是怎样的灾难!
其实,你一直都在用函数,只是没意识到。关卡揭晓了真相:当你写 hero.attack(enemy) 时,"这是在调用(calling 或者说 invoking)一个函数,名为 attack。"
调用 attack 时实际运行的代码"很长很复杂"。它要判断:"我有武器吗?我离得够近吗?挥一次要多久?我该打吗?造成多少伤害?"——所有这些细节,全被打包浓缩进了一个简单的词:attack。这正是函数的魔力:把复杂藏起来,只露出一个好用的名字。
本单元最大的跃迁是:你将从"只会调用别人写好的函数",升级到"定义自己的函数"。
函数(Function)是一段被命名、可以重复使用的代码。你把若干行代码"打包"在一起,给这个包起个名字;以后任何时候想运行这段代码,只要喊一声这个名字就行。关卡说:"函数是代码的重要组成部分。"
关卡明确指出:"定义一个函数,它含有两部分:函数名和函数体。"
| 组成部分 | 英文 | 作用 |
|---|---|---|
| 函数名 | Function Name | 函数的名称,以后用它来调用,例如 attack |
| 函数体 | Function Body | 调用函数时实际执行的那些代码 |
有时函数还会带参数(Parameter)——比如 attack(enemy) 括号里的 enemy。不过本单元先专注于不带参数的函数,参数留到以后的单元再深入。
把函数想象成一台机器:机器外面贴着名字(函数名),机器内部装着一整套零件和工序(函数体)。你不需要知道内部怎么运转,只要按下写着名字的按钮,机器就会乖乖完成整套工作。findAndAttackEnemy() 就是这样一台"找敌人并攻击"的机器——按一下,它就把找敌人、判断、攻击全做完。
在 Python 里,定义函数要用关键字 def(define 的缩写,意为"定义")。基本格式是:
def 函数名(): # 函数体:调用时要执行的代码(缩进) 要做的事情
它由四个部分组成:关键字 def、一个函数名、一对圆括号 () 加冒号 :,以及下面缩进的函数体。是不是有点眼熟?它的"冒号 + 缩进"骨架和 if、while 一脉相承。
以"乡村巡游者"里的函数为例,把"找敌人并攻击"这套逻辑完整地装进函数体:
# 这定义了 findAndAttackEnemy 函数 def findAndAttackEnemy(): enemy = hero.findNearestEnemy() if enemy: hero.attack(enemy)
函数名可以自己起,但好名字能让代码自我解释。看本单元这些名字就知道它们各自负责什么:findAndAttackEnemy(找到并攻击)、findAndCleaveEnemy(找到并劈斩)、cleaveOrAttack(劈斩或攻击)、pickUpCoin(捡硬币)、checkAndAttack(检查并攻击)。
attackEnemy。常用驼峰命名法(第一个词小写,后面每个词首字母大写)。名字里不能有空格,也不能用数字开头。
定义好函数后,调用(call / invoke)它非常简单——写下函数名,后面跟一对圆括号即可。每写一次,函数体里的代码就完整执行一遍。
while True: hero.moveXY(35, 34) # 走到第一个巡逻点 findAndAttackEnemy() # 调用!一行顶好几行 hero.moveXY(60, 31) # 走到右侧入口 findAndAttackEnemy() # 再调用一次,逻辑复用
这是新手最容易绕晕的地方:定义函数和调用函数是两个完全不同的动作。 关卡反复强调:"你需要编写函数的定义,这样你就可以在主循环中调用它了。"
| 动作 | 写法 | 发生了什么 |
|---|---|---|
| 定义 | def findAndAttackEnemy(): + 函数体 | 把代码存起来,暂不执行 |
| 调用 | findAndAttackEnemy() | 立即执行函数体里的代码 |
def 定义却忘了在循环里调用——函数永远不会运行,英雄会原地发呆。比如"造币厂"关卡就提醒你:写好 attackEnemy 函数后,记得取消 attackEnemy() 那行的注释,否则它不会生效。
关卡特别强调了一个关键区别:"调用函数时不要加 hero,因为函数由你定义,而不是英雄。"
hero.attack(enemy) # attack 是英雄自带的 → 要加 hero. findAndAttackEnemy() # 这是你定义的 → 不加 hero,直接喊名字
attack、moveXY、cleave 这些是英雄的本领,调用要写 hero. 开头;而 findAndAttackEnemy 这种你自己 def 出来的函数,是"全局"的,直接写名字加括号即可,不带 hero.。
这一节聚焦每一关如何用函数解决问题。看懂"为什么这样封装",你就能自己写出完整解法。
这是你第一次定义自己的函数。地图上有几个固定巡逻点,每个点都要做同样的"找敌人并攻击"。难点不在逻辑,而在于意识到"该把重复的部分抽出来"。策略就是把这套逻辑装进 findAndAttackEnemy(),然后在主循环里"移动 → 调用"地反复使用。
def findAndAttackEnemy(): # ① 先定义 enemy = hero.findNearestEnemy() if enemy: hero.attack(enemy) while True: # ② 再在循环里调用 hero.moveXY(35, 34) findAndAttackEnemy() # 到右侧入口再调一次……
def 定义要写在调用之前(通常放在文件顶部)。函数定义部分不在 while 循环里——它独立存在,循环里只负责"调用"。
敌人成群来袭,单靠普通攻击不够了,需要再加一个使用 cleave 的函数。这一关教你同时定义并使用多个函数:findAndAttackEnemy 负责普通攻击,findAndCleaveEnemy 负责劈斩。后者里要做两层检查——敌人存在,且 cleave 就绪。
def findAndCleaveEnemy(): enemy = hero.findNearestEnemy() if enemy: # 检查 1:敌人存在 if hero.isReady('cleave'): # 检查 2:劈斩就绪 hero.cleave(enemy)
findAndCleaveEnemy() 和 findAndAttackEnemy(),逻辑清晰得像"读说明书"。这就是把复杂藏进函数、让主流程保持简洁的威力。
这一关让你"只能靠自己"定义函数,而且要把第五单元学的嵌套 if/else 融进函数体。思路是写一个 cleaveOrAttack() 函数:有敌人时,如果 cleave 就绪就劈斩,否则就普通攻击——一个函数把"该用哪种攻击"的决策全部包办。
def cleaveOrAttack(): enemy = hero.findNearestEnemy() if enemy: if hero.isReady('cleave'): hero.cleave(enemy) else: # 劈斩没好 → 普通攻击 hero.attack(enemy)
cleaveOrAttack()",无论有几个巡逻点都一样简洁。
这一关要你"从头开始创建一个函数"。关卡给了 pickUpCoin(捡硬币)作示范,要你照葫芦画瓢,自己写一个 attackEnemy(攻击敌人)。pickUpCoin 里还用到了上一单元学的 pos:找到硬币后移动到它的 coin.pos.x, coin.pos.y 去捡。
def pickUpCoin(): # 关卡给的示例 coin = hero.findNearestItem() if coin: hero.moveXY(coin.pos.x, coin.pos.y) def attackEnemy(): # 照着写的新函数 enemy = hero.findNearestEnemy() if enemy: hero.attack(enemy)
attackEnemy() 清场,再 pickUpCoin() 捡钱。记得把示例里 attackEnemy() 那行的注释取消掉,否则函数白定义了!
这是一个概念挑战,要求很巧妙:必须定义一个函数,并且 while True 循环里只能有一条语句。 这等于强制你把"看到敌人就打、否则回中心"的全部逻辑都封装进一个函数,让主循环干净到只剩一行调用。
def checkAndAttack(): enemy = hero.findNearestEnemy() if enemy: hero.attack(enemy) else: hero.moveXY(30, 26) # 没敌人 → 回中心标记 while True: checkAndAttack() # 循环里只有这一条语句!
def 却没在循环里喊它的名字,函数永远不运行。hero.——自己定义的函数直接写名字,不要加 hero.。(),只写函数名不加括号是错的。:——def 函数名() 后面少了冒号。def 好,再去调用。恭喜你完成边际森林第六单元!你已经从"使用现成函数"进化到"创造自己的函数"。你的代码从此可以浓缩、复用、变得清晰易读。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| 函数 | 一段被命名、可复用的代码 | 全部 |
| 三大好处 | 浓缩、避免重复、易读 | 全部 |
| 函数名 / 函数体 | 名字用来调用,体是实际代码 | 全部 |
| def 定义 | def 名字(): + 缩进函数体 | 乡村巡游者 |
| 调用函数 | 写名字 + () | 全部 |
| 定义 ≠ 调用 | 定义只存代码,调用才执行 | 造币厂 |
| 不加 hero. | 自定义函数直接喊名字 | 村庄守卫 |
| 多个函数 | 各司其职,配合使用 | 村庄守卫、造币厂 |
| 函数 + 嵌套 | 把复杂判断封进函数 | 村庄守护神 |
| 极简主循环 | 循环里只剩一行调用 | 寂静市 |
会封装、懂复用的英雄,才是真正高效优雅的英雄。
继续守护边际森林吧,勇敢的程序员!🌲🛡️⚙️
CodeCombat 编程知识 · 边际森林:第六单元
CodeCombat教研团队