knight.script 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. -- ============================================================================
  2. -- KNIGHT ANIMATION STATE MACHINE - Beginner Friendly Example
  3. -- ============================================================================
  4. -- This script demonstrates how to create a character animation system using
  5. -- a simple implementation of a Finite State Machine (FSM) in Defold.
  6. -- Input action hashes - these connect keyboard/gamepad buttons to our code
  7. -- In Defold, we use hash() to convert strings to efficient identifiers
  8. local INPUT = {
  9. JUMP = hash("jump"),
  10. CROUCH = hash("crouch"),
  11. ATTACK = hash("attack"),
  12. LEFT = hash("left"),
  13. RIGHT = hash("right")
  14. }
  15. -- ============================================================================
  16. -- STATE MACHINE CONFIGURATION
  17. -- ============================================================================
  18. -- This table defines ALL possible states our character can be in.
  19. -- Think of it as a "rule book" that tells the game:
  20. -- - What animation to play in each state
  21. -- - Whether the animation should loop or play once
  22. -- - What should happen when the player presses different buttons
  23. --
  24. -- Each state is like a "mode" the character is in. For example:
  25. -- - "standing_idle" = character is standing still, playing idle animation, looped
  26. --
  27. -- The "on_" properties define what happens when inputs are pressed, e.g.:
  28. -- - on_attack = what state to go to when attack button is pressed
  29. -- - on_move = what state to go to when movement keys are pressed
  30. -- - default_next = what state to go to when animation finishes (for non-looped animations)
  31. local STATE_CONFIG = {
  32. -- STANDING STATES - Character is upright and can move freely
  33. -- These are the "normal" states when the character is standing
  34. standing_idle = {
  35. animation = "idle", -- Play the "idle" animation from the sprite atlas
  36. is_looped = true, -- Keep playing this animation over and over
  37. on_crouch = "to_crouch", -- If crouch key pressed, go to "to_crouch" state
  38. on_attack = "standing_attack", -- If attack key pressed, go to "standing_attack" state
  39. on_jump = "standing_jump", -- If jump key pressed, go to "standing_jump" state
  40. on_move = "standing_run", -- If movement keys pressed, go to "standing_run" state
  41. on_turn = "standing_turn" -- If character turns around, go to "standing_turn" state
  42. },
  43. standing_run = {
  44. animation = "run", -- Play the running animation
  45. is_looped = true, -- Loop the running animation continuously
  46. on_crouch = "to_crouch", -- Can still crouch while running
  47. on_attack = "standing_attack", -- Can attack while running
  48. on_jump = "standing_jump", -- Can jump while running
  49. on_stop = "standing_idle", -- When movement stops, go back to idle
  50. on_turn = "standing_turn" -- When turning around, play turn animation
  51. },
  52. standing_jump = {
  53. animation = "jump", -- Play the jump animation
  54. is_looped = false, -- Play jump animation only once
  55. default_next = "standing_idle" -- When jump animation finishes, go back to idle
  56. },
  57. standing_attack = {
  58. animation = "attack", -- Play the attack animation
  59. is_looped = false, -- Play attack animation only once
  60. default_next = "standing_idle" -- When attack finishes, go back to idle
  61. },
  62. standing_turn = {
  63. animation = "turn_around", -- Play the turn around animation
  64. is_looped = false, -- Play turn animation only once
  65. default_next = "standing_idle", -- When turn finishes, go to idle
  66. on_turn = "standing_turn" -- If turning again while already turning, keep turning
  67. },
  68. -- CROUCHING STATES - Character is in low position, limited movement
  69. -- When crouching, the character can't jump but can still move and attack
  70. crouching_idle = {
  71. animation = "crouch_idle", -- Play the crouching idle animation
  72. is_looped = true, -- Loop the crouch idle animation
  73. on_stand = "to_standing", -- If crouch key released, start standing up
  74. on_attack = "crouching_attack", -- Can attack while crouching
  75. on_move = "crouching_run" -- Can move while crouching (crouch walk)
  76. },
  77. crouching_run = {
  78. animation = "crouch_walk", -- Play the crouch walking animation
  79. is_looped = true, -- Loop the crouch walk animation
  80. on_stand = "to_standing", -- Can stand up while crouch walking
  81. on_attack = "crouching_attack", -- Can attack while crouch walking
  82. on_stop = "crouching_idle" -- When movement stops, go to crouch idle
  83. },
  84. crouching_attack = {
  85. animation = "crouch_attack", -- Play the crouch attack animation
  86. is_looped = false, -- Play attack animation only once
  87. default_next = "crouching_idle", -- When attack finishes, go to crouch idle
  88. on_stand = "to_standing", -- Can stand up even while attacking
  89. },
  90. -- TRANSITION STATES - Intermediate animations between major state changes
  91. -- These states handle the smooth transition between standing and crouching
  92. to_crouch = {
  93. animation = "to_crouch", -- Play the "going into crouch" animation
  94. is_looped = false, -- Play transition animation only once
  95. default_next = "crouching_idle" -- When transition finishes, go to crouch idle
  96. },
  97. to_standing = {
  98. animation = "from_crouch", -- Play the "standing up from crouch" animation
  99. is_looped = false, -- Play transition animation only once
  100. default_next = "standing_idle" -- When transition finishes, go to standing idle
  101. }
  102. }
  103. -- ============================================================================
  104. -- MOVEMENT AND DIRECTION LOGIC
  105. -- ============================================================================
  106. --- Updates movement state and sprite direction based on input
  107. --- This function figures out:
  108. --- 1. Is the character moving? (left or right key pressed)
  109. --- 2. Which direction is the character facing? (left or right)
  110. --- 3. Did the character just turn around? (for turn animation)
  111. --- @param self table Script instance with input flags
  112. local function update_movement_state(self)
  113. -- Start by assuming the character is not moving
  114. self.is_moving = false
  115. -- Remember the previous facing direction to detect turns
  116. local previous_is_flipped = self.is_flipped
  117. -- Check movement input and update facing direction
  118. if self[INPUT.LEFT] and not self[INPUT.RIGHT] then
  119. -- Left key is pressed and right key is not pressed
  120. self.is_moving = true
  121. self.is_flipped = true -- Character faces left (sprite is flipped)
  122. elseif self[INPUT.RIGHT] and not self[INPUT.LEFT] then
  123. -- Right key is pressed and left key is not pressed
  124. self.is_moving = true
  125. self.is_flipped = false -- Character faces right (sprite is not flipped)
  126. end
  127. -- If both keys are pressed or neither is pressed, character doesn't move
  128. -- Detect if the character just turned around - used to trigger the "turn around" animation
  129. self.is_turning = self.is_flipped ~= previous_is_flipped
  130. end
  131. -- ============================================================================
  132. -- STATE TRANSITION LOGIC
  133. -- ============================================================================
  134. --- Determines the next state based on current input and state configuration
  135. --- This is the "brain" of our state machine - it decides what state to go to next
  136. ---
  137. --- INPUT PRIORITY SYSTEM (in order of importance):
  138. --- 1. Attack - Highest priority, can interrupt most other actions
  139. --- 2. Jump - High priority, can interrupt movement
  140. --- 3. Movement - Medium priority, handles start/stop moving
  141. --- 4. Crouch/Stand - Medium priority, changes posture
  142. --- 5. Turn - Lowest priority, only when changing direction
  143. ---
  144. --- @param self table Script instance with input flags and current state
  145. --- @return string|nil Next state name or nil if no transition needed
  146. local function get_next_state(self)
  147. -- Get current input state and configuration
  148. local is_crouching = self[INPUT.CROUCH] -- Is crouch key currently pressed?
  149. local config = STATE_CONFIG[self.state] -- Get rules for current state
  150. local next_state = nil -- Will hold the next state to go to
  151. -- PRIORITY 1: ATTACK INPUT (Highest Priority)
  152. -- Attack can interrupt almost any other action
  153. if self[INPUT.ATTACK] then
  154. next_state = config.on_attack -- Go to attack state if current state allows it
  155. end
  156. -- PRIORITY 2: JUMP INPUT (High Priority)
  157. -- Jump can interrupt movement but not attack
  158. if self[INPUT.JUMP] then
  159. next_state = config.on_jump -- Go to jump state if current state allows it
  160. end
  161. -- PRIORITY 3: MOVEMENT STATE CHANGES (Medium Priority)
  162. -- Handle starting to move or stopping movement
  163. if self.is_moving and config.on_move then
  164. next_state = config.on_move -- Character is moving and current state has a "move" transition
  165. elseif not self.is_moving and config.on_stop then
  166. next_state = config.on_stop -- Character stopped moving and current state has a "stop" transition
  167. end
  168. -- PRIORITY 4: CROUCH/STAND STATE CHANGES (Medium Priority)
  169. -- Handle posture changes (standing vs crouching)
  170. if is_crouching and config.on_crouch then
  171. next_state = config.on_crouch -- Crouch key is pressed and current state allows crouching
  172. elseif not is_crouching and config.on_stand then
  173. next_state = config.on_stand -- Crouch key is released and current state allows standing
  174. end
  175. -- PRIORITY 5: DIRECTION CHANGE (Lowest Priority)
  176. -- Handle turning around (only when changing direction)
  177. if self.is_turning and config.on_turn then
  178. next_state = config.on_turn -- Character just turned around and current state has turn animation
  179. end
  180. -- Return the next state (or nil if no transition is needed)
  181. return next_state
  182. end
  183. -- ============================================================================
  184. -- VISUAL LAYER - Handles all visual effects and animations
  185. -- ============================================================================
  186. --- Updates all visual elements based on current character state
  187. --- This function is responsible for making the character look correct on screen:
  188. --- - Playing the right animation for the current state
  189. --- - Flipping the sprite to face the right direction
  190. --- - Creating special effects (like the jump animation)
  191. --- - Updating the GUI to show current state
  192. --- @param self table Script instance with current state and flip information
  193. local function update_visuals(self)
  194. -- Get the configuration for the current state
  195. local config = STATE_CONFIG[self.state]
  196. -- Play the animation for the current state
  197. sprite.play_flipbook("#sprite", config.animation)
  198. -- Visualize the jump effect
  199. -- (When jumping, we add a visual effect by moving the character up and down)
  200. if self.state == "standing_jump" then
  201. local pos = go.get_position()
  202. -- Animate the Y position to simulate a jump visually
  203. go.animate(".", "position.y", go.PLAYBACK_ONCE_PINGPONG, pos.y + 50, go.EASING_INOUTCUBIC, 0.6)
  204. else
  205. -- If not jumping, make sure any jump animation is cancelled and reset the character to ground level
  206. go.cancel_animations(".", "position.y")
  207. local pos = go.get_position()
  208. go.set_position(vmath.vector3(pos.x, 600, pos.z)) -- 600 is our ground level Y position
  209. end
  210. -- Update the GUI - send a message to the GUI component to update the UI
  211. msg.post("gui", "animation_state_changed", {
  212. state = self.state
  213. })
  214. end
  215. -- ============================================================================
  216. -- DEFOLD LIFECYCLE FUNCTIONS
  217. -- ============================================================================
  218. --- Initializes the knight character when the game starts
  219. --- It sets up the initial state and prepares the character for input
  220. --- @param self table Script instance - this is automatically provided by Defold
  221. function init(self)
  222. -- Set up initial state machine state as "standing_idle"
  223. self.state = "standing_idle"
  224. -- Set up movement and direction flags
  225. self.is_flipped = false -- Character starts facing right (not flipped)
  226. self.is_moving = false -- Character starts not moving
  227. self.is_turning = false -- Character starts not turning
  228. -- Initialize all input flags - start with all keys "not pressed" (false)
  229. self[INPUT.LEFT] = false -- Left arrow key
  230. self[INPUT.RIGHT] = false -- Right arrow key
  231. self[INPUT.JUMP] = false -- Space bar
  232. self[INPUT.ATTACK] = false -- Attack button (X)
  233. self[INPUT.CROUCH] = false -- Crouch button (C)
  234. -- Display the initial state visually
  235. update_visuals(self)
  236. -- Enable input handling
  237. msg.post(".", "acquire_input_focus")
  238. end
  239. --- Handles input events from keyboard every time the player presses or releases a key
  240. --- It updates our input tracking and triggers state transitions
  241. --- @param self table Script instance
  242. --- @param action_id hash Which input was pressed (like "jump", "attack", etc.)
  243. --- @param action table Contains information about the input (pressed/released)
  244. function on_input(self, action_id, action)
  245. -- Update input state - keep track of which keys are currently being pressed:
  246. if action.pressed then
  247. self[action_id] = true -- Key was just pressed down
  248. elseif action.released then
  249. self[action_id] = false -- Key was just released
  250. end
  251. -- Process state machine:
  252. update_movement_state(self) -- Update movement and direction state based on input
  253. local next_state = get_next_state(self) -- Decide what state to go to next
  254. -- If we determined a new state is needed, switch to it and update visuals:
  255. if next_state then
  256. self.state = next_state -- Change to the new state
  257. update_visuals(self) -- Update the visual appearance
  258. end
  259. end
  260. --- Handles messages from other game objects
  261. --- We use it to handle messages that comes to the script when animations finish playing
  262. --- @param self table Script instance
  263. --- @param message_id hash What type of message this is
  264. function on_message(self, message_id, message)
  265. -- This message is sent when a non-looped animation finishes playing (like attack, jump, or turn animations)
  266. if message_id == hash("animation_done") then
  267. -- Flip the sprite horizontally when the character just finished turning
  268. if message.id == hash("turn_around") then
  269. sprite.set_hflip("#sprite", self.is_flipped)
  270. end
  271. -- Process state machine:
  272. update_movement_state(self) -- Update movement and direction state based on input
  273. local next_state = get_next_state(self) -- Decide what state to go to next
  274. -- Switch to the next state (or default next) and update visuals
  275. self.state = next_state or STATE_CONFIG[self.state].default_next
  276. update_visuals(self)
  277. end
  278. end