A Third-Person-Shooter Godot game.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

169 lines
6.1 KiB

extends KinematicBody
enum State {
APPROACH = 0,
AIM = 1,
SHOOTING = 2,
}
const PLAYER_AIM_TOLERANCE_DEGREES = 15
const SHOOT_WAIT = 0.5
const AIM_TIME = 1
const AIM_PREPARE_TIME = 0.5
const BLEND_AIM_SPEED = 0.05
export(int) var health = 5
export(bool) var test_shoot = false
var state = State.APPROACH
var shoot_countdown = SHOOT_WAIT
var aim_countdown = AIM_TIME
var aim_preparing = AIM_PREPARE_TIME
var dead = false
var player = null
var velocity = Vector3()
var orientation = Transform()
const ROTATION_INTERPOLATE_SPEED = 0.025
var root_motion = Transform()
var motion = Vector2()
var last_player_sighting = Vector3()
onready var initial_position = transform.origin
onready var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
onready var animation_tree = $AnimationTree
onready var player_model = $PlayerModel
onready var shoot_from = player_model.get_node(@"Colette_Armature/Skeleton/GunBone/ShootFrom")
onready var fire_cooldown = $FireCooldown
onready var collision_shape = $CapsuleShape
onready var sound_effects = $SoundEffects
onready var sound_effect_jump = sound_effects.get_node(@"Jump")
onready var sound_effect_land = sound_effects.get_node(@"Land")
onready var sound_effect_shoot = sound_effects.get_node(@"Shoot")
onready var explosion_sound = sound_effects.get_node(@"Explosion")
onready var hit_sound = sound_effects.get_node(@"Hit")
onready var debug_marker = $debug_marker
func _ready():
# Pre-initialize orientation transform.
orientation = player_model.global_transform
orientation.origin = Vector3()
func _physics_process(delta):
if dead:
return
if player != null and fire_cooldown.time_left == 0:
# See if player can be killed because in they're sight.
var ray_origin = shoot_from.global_transform.origin
var ray_to = player.global_transform.origin + Vector3.UP # Above middle of player.
var col = get_world().direct_space_state.intersect_ray(ray_origin, ray_to, [self], 0b111)
# If our raycast hit a player
if not col.empty() and col.collider == player:
# Record last sighting (in case they hide later)
last_player_sighting = ray_to
state = State.AIM
aim_countdown = AIM_TIME
aim_preparing = 0
animation_tree["parameters/state/current"] = 1
# If the player is detected by at too large an angle, slerp rotate towards them
var to_player_local = global_transform.xform_inv(player.global_transform.origin)
# The front of this is +Z, and atan2 is zero at +X, so we need to use the Z for the X parameter (second one).
var angle_to_player = atan2(to_player_local.x, to_player_local.z)
var tolerance = deg2rad(PLAYER_AIM_TOLERANCE_DEGREES)
if angle_to_player > tolerance:
lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED)
debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3)
elif angle_to_player < -tolerance:
lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED)
debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3)
else:
# Facing player, try to shoot.
self.look_at(ray_to, Vector3.UP)
self.rotate_object_local(Vector3.UP, PI)
var bullet = preload("res://player/bullet/bullet.tscn").instance()
get_parent().add_child(bullet)
bullet.global_transform.origin = ray_origin
# If we don't rotate the bullets there is no useful way to control the particles ..
bullet.look_at(ray_to, Vector3.UP)
bullet.add_collision_exception_with(self)
var shoot_particle = $PlayerModel/Colette_Armature/Skeleton/GunBone/ShootFrom/ShootParticle
shoot_particle.restart()
shoot_particle.emitting = true
var muzzle_particle = $PlayerModel/Colette_Armature/Skeleton/GunBone/ShootFrom/MuzzleFlash
muzzle_particle.restart()
muzzle_particle.emitting = true
fire_cooldown.start()
sound_effect_shoot.play()
# Player not in sight.
shoot_countdown = SHOOT_WAIT
# Run at their last known position
if last_player_sighting != Vector3():
# If the player is detected by at too large an angle, slerp rotate towards them
var to_player_local = global_transform.xform_inv(last_player_sighting)
# The front of this is +Z, and atan2 is zero at +X, so we need to use the Z for the X parameter (second one).
var angle_to_player = atan2(to_player_local.x, to_player_local.z)
var tolerance = deg2rad(PLAYER_AIM_TOLERANCE_DEGREES)
if angle_to_player > tolerance:
lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED)
debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3)
elif angle_to_player < -tolerance:
lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED)
debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3)
animation_tree["parameters/state/current"] = 2
# Blend position for walk speed based on motion.
# Run animation straight forward, high speed
animation_tree["parameters/walk/blend_position"] = Vector2(0.5, 0)
# move in XY direction facing
root_motion = global_transform.basis.z * 3# * delta
move_and_slide(root_motion.length() * self.transform.basis.z * Vector3(1,0,1), Vector3.UP)
if get_slide_count() == 0:
move_and_slide(Vector3.DOWN * 9.8, Vector3.UP)
# else: # Not in air or aiming, idle.
func lerp_to_face_player(angle_to_player, rot_speed):
var langle = lerp_angle(0, angle_to_player, rot_speed)
# restrict to Y axis (no vertical rotation)
self.global_transform.basis = Basis(Vector3(0, 1, 0), langle) * self.global_transform.basis
func hit(col):
health -= 1
if health <= 0:
dead = true
animation_tree.active = false
player_model.visible = false
collision_shape.disabled = true
explosion_sound.play()
self.queue_free() # delete self
hit_sound.play()
func _on_Area_body_entered(body):
if body.name == "Player" or body.name == "Target":
player = body
animation_tree["parameters/idle_aim/current"] = 1
func _on_Area_body_exited(body):
if body.name == "Player":
player = null
animation_tree["parameters/idle_aim/current"] = 0