GridBasedMovable.gd 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. extends CharacterBody3D
  2. class_name GridBasedMovable
  3. #signals
  4. signal target_changed(target_pos)
  5. signal pushed(direction)
  6. signal tween_started()
  7. #enums
  8. #fixme: make DIRECTIONS unique
  9. enum DIRECTIONS {UP, DOWN, LEFT, RIGHT, NONE}
  10. #constants
  11. const GRAVITY = -20
  12. #exports
  13. @export var step_size = 2
  14. @export var step_time = 0.15
  15. @export var stop_time = 0.0
  16. @export var can_slide = false
  17. @export var enable_gravity = true
  18. @export var invincible = false
  19. @export var vulnerable_to_spikes = true
  20. @export var enable_rotation = false
  21. #vars
  22. var requested_direction = DIRECTIONS.NONE
  23. var local_tween_position = Vector3()
  24. var target_position = Vector3()
  25. var distant_target_position = Vector3()
  26. var gravity_applied = false
  27. var enable_movement = true
  28. var level
  29. var speed_modifier = 1.0
  30. var already_hit_by_spikes = false
  31. var animated_mesh = null
  32. var has_animated_mesh = false
  33. var mesh_direction
  34. var animation_player = null
  35. #onready
  36. @onready var rc_holder = $RC_Holder
  37. @onready var ray_front = $RC_Holder/RayCastFront
  38. @onready var ray_downramp = $RC_Holder/RayCastDownRamp
  39. @onready var ray_front_pull = $RC_Holder/RayCastFrontPull
  40. @onready var ray_back = $RC_Holder/RayCastBack
  41. @onready var ray_ground = $RC_Holder/RayCastGround
  42. @onready var ray_top = $RC_Holder/RayCastTop
  43. @onready var rotation_point = $RotationPoint
  44. @onready var mesh_instance = $RotationPoint/StaticMesh
  45. @onready var detection_area = $DetectionArea
  46. @onready var protection_area = $RotationPoint/PlacementProtectionArea
  47. @onready var timer = $Timer
  48. @onready var SMR_timer = $SpeedModResetTimer
  49. @onready var health_node = $Health
  50. #----------- built-in functions -----------
  51. func _ready() -> void:
  52. animated_mesh = find_child("AnimatedMesh")
  53. if animated_mesh != null:
  54. has_animated_mesh = true
  55. animated_mesh.visible = true
  56. mesh_instance.visible = false
  57. animation_player = animated_mesh.find_child("AnimationPlayer")
  58. position.x = position.snapped(Vector3(step_size, step_size, step_size) - Vector3(1,0,1)).x
  59. position.z = position.snapped(Vector3(step_size, step_size, step_size) - Vector3(1,0,1)).z
  60. requested_direction = DIRECTIONS.NONE
  61. local_tween_position = position
  62. timer.wait_time = stop_time + (step_time * speed_modifier)
  63. target_position = position
  64. distant_target_position = position
  65. update_detection_area()
  66. level = get_tree().get_nodes_in_group("level")[0]
  67. already_hit_by_spikes = false
  68. func _physics_process(delta):
  69. _handle_movement(delta)
  70. #----------- movement related -----------
  71. func _handle_movement(delta):
  72. update_detection_area()
  73. #reset the raycast to be disabled when not moving
  74. #_update_raycast_direction(DIRECTIONS.NONE)
  75. rc_holder.update_raycast_direction(DIRECTIONS.NONE)
  76. gravity_applied = false
  77. if is_grid_aligned():
  78. if !is_on_floor():
  79. if enable_gravity:
  80. velocity.y += GRAVITY * delta
  81. gravity_applied = true
  82. target_position.y = position.y
  83. distant_target_position.y = position.y
  84. #else:
  85. if can_slide:
  86. _handle_slope_sliding()
  87. move_to(requested_direction)
  88. else:
  89. requested_direction = DIRECTIONS.NONE
  90. if !ray_ground.is_colliding() and on_floor_or_flying() and !gravity_applied and enable_gravity:
  91. velocity.y += GRAVITY * delta * 6
  92. position.x = local_tween_position.x
  93. position.z = local_tween_position.z
  94. if ray_ground.is_colliding():
  95. var groundObject = ray_ground.get_collider()
  96. if groundObject.is_in_group("spiky") and vulnerable_to_spikes == true and invincible == false:
  97. if already_hit_by_spikes == false:
  98. hit()
  99. already_hit_by_spikes = true
  100. move_and_slide()
  101. func can_move_to(direction: DIRECTIONS) -> bool:
  102. if direction != DIRECTIONS.NONE and is_grid_aligned() and on_floor_or_flying() and is_target_free(direction):
  103. rc_holder.update_raycast_direction(direction)
  104. return !ray_front.is_colliding()
  105. return false
  106. func move_to(direction, force_move: bool = false, instant: bool = false):
  107. #print ("move_to called: " + str(instant))
  108. if direction != DIRECTIONS.NONE and is_grid_aligned():
  109. rc_holder.update_raycast_direction(direction)
  110. update_rotation_point(direction)
  111. if (!ray_front.is_colliding() and is_target_free(direction) and on_floor_or_flying()) or force_move == true:
  112. if !instant:
  113. if timer.get_time_left() == 0:
  114. timer.start()
  115. _movement_tween(direction)
  116. else:
  117. print ("................")
  118. _instant_move(direction)
  119. func movement_started():
  120. pass
  121. func _movement_tween(direction: DIRECTIONS):
  122. #called in move_to, part of the actual movement, the vector "local_tween_position" is animated here
  123. #later, in _handle_movement, the actual position is set (only x and z coords)
  124. if not enable_movement:
  125. return
  126. var movedir = direction_to_vector(direction)
  127. var tween := create_tween()
  128. if tween == null:
  129. #fixes ugly crash with tween = null ;-)
  130. return
  131. tween.stop()
  132. tween.set_trans(Tween.TRANS_LINEAR)
  133. tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
  134. target_position = position + movedir * step_size
  135. distant_target_position = target_position + movedir * step_size
  136. emit_signal("target_changed", target_position)
  137. tween.tween_property(self, "local_tween_position", target_position, step_time * speed_modifier)
  138. #print (str(name) + " starts movement, target pos: " + str(target_position))
  139. tween.play()
  140. emit_signal("tween_started")
  141. movement_started()
  142. already_hit_by_spikes = false
  143. func _instant_move(direction: DIRECTIONS):
  144. if not enable_movement:
  145. return
  146. var movedir = direction_to_vector(direction)
  147. target_position = position + movedir * step_size
  148. distant_target_position = target_position + movedir * step_size
  149. emit_signal("target_changed", target_position)
  150. local_tween_position = target_position
  151. position.x = target_position.x
  152. position.z = target_position.z
  153. func modify_speed_timed(amount, seconds = 0.01):
  154. #print ("modify speed")
  155. speed_modifier = amount
  156. SMR_timer.wait_time = seconds
  157. SMR_timer.start()
  158. func _on_speed_mod_reset_timer_timeout() -> void:
  159. speed_modifier = 1.0
  160. #print ("reset speed")
  161. func _handle_slope_sliding():
  162. if is_on_slope() and requested_direction == DIRECTIONS.NONE:
  163. move_and_slide()
  164. if get_floor_normal().x > 0.2:
  165. requested_direction = DIRECTIONS.RIGHT
  166. elif get_floor_normal().x < -0.2:
  167. requested_direction = DIRECTIONS.LEFT
  168. elif get_floor_normal().z > 0.2:
  169. requested_direction = DIRECTIONS.DOWN
  170. elif get_floor_normal().z < -0.2:
  171. requested_direction = DIRECTIONS.UP
  172. #----------- push and pull -----------
  173. func push(object_to_push: GridBasedMovable, direction: DIRECTIONS, force: bool = false, instant: bool = false):
  174. #print ("push......!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
  175. #prevents side pushing
  176. if object_to_push.enable_movement == false:
  177. return
  178. if direction == DIRECTIONS.UP or direction == DIRECTIONS.DOWN:
  179. if position.x != object_to_push.position.x:
  180. return
  181. elif direction == DIRECTIONS.LEFT or direction == DIRECTIONS.RIGHT:
  182. if position.z != object_to_push.position.z:
  183. return
  184. if object_to_push.can_move_to(direction) and object_to_push.timer.get_time_left() == 0:
  185. object_to_push.move_to(direction, force, instant)
  186. object_to_push.emit_signal("pushed", direction)
  187. move_to(direction, true, instant)
  188. return true
  189. return false
  190. func can_be_pulled_to(direction: DIRECTIONS) -> bool:
  191. rc_holder.update_raycast_direction(direction)
  192. if ray_front_pull.is_colliding():
  193. var _frontObject = ray_front_pull.get_collider()
  194. if _frontObject != null:
  195. print ("front collision: " + str(_frontObject.name))
  196. return false
  197. return true
  198. func get_push_down_ramp_object():
  199. if requested_direction != DIRECTIONS.NONE and is_on_slope() and ray_downramp.is_colliding() and is_grid_aligned():
  200. var downramp = ray_downramp.get_collider()
  201. if downramp != null:
  202. if downramp.is_in_group("pushable"):
  203. print ("downramp object detected")
  204. return downramp
  205. elif downramp.get_parent().is_in_group("pushable"):
  206. return downramp.get_parent()
  207. return null
  208. #----------- physics related -----------
  209. func on_floor_or_flying() -> bool:
  210. return is_on_floor() or enable_gravity == false
  211. func is_on_slope() -> bool:
  212. return get_floor_normal().y <= 0.96 and on_floor_or_flying()
  213. func hit():
  214. pass
  215. func update_detection_area():
  216. detection_area.position.x = target_position.x - position.x
  217. rc_holder.position.x = target_position.x - position.x
  218. detection_area.position.z = target_position.z - position.z
  219. rc_holder.position.z = target_position.z - position.z
  220. ray_front.force_raycast_update()
  221. ray_back.force_raycast_update()
  222. ray_ground.force_raycast_update()
  223. ray_downramp.force_raycast_update()
  224. ray_front_pull.force_raycast_update()
  225. func print_front_collision():
  226. if ray_front.is_colliding():
  227. var frontObject = ray_front.get_collider()
  228. if frontObject != null:
  229. print ("front collision: " + str(frontObject.name))
  230. else:
  231. print ("front object is null")
  232. else:
  233. print ("no front collision")
  234. func print_pull_collision():
  235. if ray_front_pull.is_colliding():
  236. var frontObject = ray_front_pull.get_collider()
  237. if frontObject != null:
  238. print ("pull collision: " + str(frontObject.name))
  239. else:
  240. print ("pull object is null")
  241. else:
  242. print ("no pull collision")
  243. func print_back_collision():
  244. if ray_back.is_colliding():
  245. var backObject = ray_back.get_collider()
  246. if backObject != null:
  247. print ("back collision: " + str(backObject.name))
  248. else:
  249. print ("back object is null")
  250. else:
  251. print ("no back collision")
  252. #----------- helper -----------
  253. func is_target_free(direction: DIRECTIONS):
  254. var target2 = position + direction_to_vector(direction) * step_size
  255. return level.is_target_free(self, target2)
  256. func is_grid_aligned() -> bool:
  257. return abs(fmod(position.x - 1, step_size)) == 0 and abs(fmod(position.z - 1, step_size)) == 0
  258. func get_opposite_direction(direction):
  259. if direction == DIRECTIONS.LEFT:
  260. return DIRECTIONS.RIGHT
  261. elif direction == DIRECTIONS.RIGHT:
  262. return DIRECTIONS.LEFT
  263. elif direction == DIRECTIONS.UP:
  264. return DIRECTIONS.DOWN
  265. elif direction == DIRECTIONS.DOWN:
  266. return DIRECTIONS.UP
  267. else:
  268. return DIRECTIONS.NONE
  269. func direction_to_vector(direction) -> Vector3:
  270. var dir = Vector3(0, 0, 0)
  271. if direction == DIRECTIONS.UP:
  272. dir = Vector3(0, 0, -1)
  273. elif direction == DIRECTIONS.DOWN:
  274. dir = Vector3(0, 0, 1)
  275. elif direction == DIRECTIONS.LEFT:
  276. dir = Vector3(-1, 0, 0)
  277. elif direction == DIRECTIONS.RIGHT:
  278. dir = Vector3(1, 0, 0)
  279. return dir
  280. func update_rotation_point(direction):
  281. if not enable_rotation:
  282. mesh_direction = DIRECTIONS.NONE
  283. return
  284. if direction != DIRECTIONS.NONE:
  285. mesh_direction = direction
  286. match direction:
  287. DIRECTIONS.UP:
  288. rotation_point.rotation = Vector3(0,0,0)
  289. DIRECTIONS.DOWN:
  290. rotation_point.rotation = Vector3(0,deg_to_rad(180),0)
  291. DIRECTIONS.LEFT:
  292. rotation_point.rotation = Vector3(0,deg_to_rad(90),0)
  293. DIRECTIONS.RIGHT:
  294. rotation_point.rotation = Vector3(0,deg_to_rad(-90),0)
  295. update_detection_area()
  296. rc_holder.update_raycast_direction(direction)
  297. func check_distant_object():
  298. if not is_grid_aligned():
  299. return level.find_object_at_position(self, distant_target_position)
  300. return null
  301. func check_current_target_object():
  302. if is_grid_aligned():
  303. var target_pos = position + direction_to_vector(mesh_direction) * step_size
  304. return level.find_object_at_position(self, target_pos)
  305. return null