概述

当我们初学时,很多教程都教授我们使用类似is_jumping、is_air之类的布尔类型的变量表示玩家是否处于某个状态,然后在_process()或_physics_process()中每帧去侦测按键输入,然后再根据输入去改变上面的状态变量或通过判断执行相应的行为。
然而当使用这种简单粗暴的方式,为了实现玩家不同的状态和行为,我们或许会声明十几二十个这样的变量,并用各种if语句和嵌套的if语句把自己的代码搞得一团乱麻。
为此,我们引入新的方式来重构我们的代码。

原始代码

为了比较效果,这里我们还是贴上上一节基础平台移动和跳跃的代码。

image.png image.png

原始代码

  1. extends KinematicBody2D
  2. var speed = 60.0 # 速度 像素/秒
  3. var velocity = Vector2.ZERO # 速度向量
  4. var gravity = 3000 # 重力加速度
  5. var jump_force = 3000.0 # 单次跳跃高度
  6. var is_jumping = false
  7. func _process(delta):
  8. velocity = Vector2.ZERO
  9. # 左右移动
  10. velocity.x = Input.get_axis("ui_left","ui_right") * speed
  11. # 实现下落
  12. velocity.y += gravity * delta
  13. # 落地
  14. if is_on_floor() and is_jumping == true:
  15. is_jumping = false
  16. return
  17. # 跳跃
  18. if Input.is_action_just_pressed("ui_accept"):
  19. if is_on_floor(): # 在地面
  20. velocity.y = -jump_force
  21. is_jumping = true
  22. else: # 在空中
  23. if is_jumping: # 正在跳跃
  24. # 二级跳
  25. velocity.y = -jump_force
  26. is_jumping = false
  27. velocity = move_and_slide(velocity,Vector2.UP)

重构代码

然后我们对上面的代码进行重构。

image.png image.png

原始代码

  1. extends KinematicBody2D
  2. var speed = 60.0 # 速度 像素/秒
  3. var velocity = Vector2.ZERO # 速度向量
  4. var gravity = 3000 # 重力加速度
  5. var jump_force = 3000.0 # 单次跳跃高度
  6. var is_jumping = false
  7. enum states {
  8. ON_FLOOR, # 在地面上
  9. IN_AIR # 在空中
  10. }
  11. var _state = states.ON_FLOOR
  12. func _physics_process(delta):
  13. velocity = Vector2.ZERO
  14. match _state:
  15. # 在地面
  16. states.ON_FLOOR:
  17. # 边缘掉落
  18. if not is_on_floor():
  19. change_state_to(states.ON_FLOOR)
  20. is_jumping = false
  21. return
  22. # 左右移动
  23. velocity.x = Input.get_axis("ui_left","ui_right") * speed
  24. # 实现下落 -- 用于边缘下落
  25. velocity.y += gravity * delta
  26. # 跳跃
  27. if Input.is_action_just_pressed("ui_accept"):
  28. velocity.y = -jump_force
  29. change_state_to(states.IN_AIR)
  30. is_jumping = true
  31. velocity = move_and_slide(velocity,Vector2.UP)
  32. # 在空中
  33. states.IN_AIR:
  34. # 掉到地板
  35. if is_on_floor():
  36. change_state_to(states.ON_FLOOR)
  37. is_jumping = false
  38. return
  39. # 左右移动
  40. velocity.x = Input.get_axis("ui_left","ui_right") * speed
  41. # 实现下落 -- 用于边缘下落
  42. velocity.y += gravity * delta
  43. # 跳跃 -- 二级跳
  44. if Input.is_action_just_pressed("ui_accept") and is_jumping:
  45. velocity.y = -jump_force
  46. change_state_to(states.IN_AIR)
  47. is_jumping = false
  48. velocity = move_and_slide(velocity,Vector2.UP)
  49. pass
  50. # 更改状态
  51. func change_state_to(new_state:int):
  52. _state = new_state
  53. # 根据不同的状态,进行一些额外配置
  54. match _state:
  55. states.IN_AIR:
  56. pass
  57. states.ON_FLOOR:
  58. pass

代码分析

我们声明一个枚举states,然后书写两个表示在地面和空中的常量ONFLOOR和IN_AIR。然后声明_state变量存储当前玩家所处的状态,然后在_physics_process()中用match语句去检测_state变量当前的值,也就是玩家当前的状态,然后再根据具体的状态去进行相应的每帧处理。
可以看到,貌似我们这样写,代码比原来还多了,而且有相当一部分的代码重复,这是因为之前的写法共用了两个状态都能用的代码,而现在为了进行不同状态代码之间的完全区分,或者叫状态隔离。我们必须牺牲一些复用性。
这样写的好处是,当我们继续实现诸如爬墙、爬梯子或其他什么功能和状态时,我们可以继续简单的扩充我们的状态枚举states,然后轻松的用match语句的一个分支来进行这些状态下的处理。而不是再搞一堆is
打头的变量,并嵌套各种if语句。
这是一个很好的进步,你可以用这种方式重构你的玩家或NPC的代码,精确而又无痛的处理它们的状态和行为。而这种状态隔离处理的思维,也是有限状态机的基础。
另外,我们还将给state变量赋值,封装为一个函数change_state_to()。这样一方面可以呈现更明显的语义,另一方面在其中我们利用另一个match语句,可以在状态切换后的瞬间对各个状态进行一定的统一配置。
当然我们并没有完全消灭is
打头的变量,我们保留了is_jumping变量 ,通过和IN_AIR状态搭配,可以用来区分玩家是第一次跳还是第二次跳,从而实现二级跳的效果。

运行效果

运行效果基本上与上一节的没有区别。此处就不再展示。