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.
 
 
 

190 lines
6.7 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()
var dummy : Spatial
var in_detection_normal = false
var in_detection_focused = false
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 = $merc1/CharacterRig001
onready var shoot_from = player_model.get_node(@"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 reaction_time = $ReactionTime.get_wait_time()
onready var sight_normal = $"PlayerDetection1/NormalLoS"
onready var sight_focused = $"PlayerDetection1/AttentiveLoS"
func _ready():
# Pre-initialize orientation transform.
orientation = player_model.global_transform
orientation.origin = Vector3()
dummy = Spatial.new()
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 $ReactionTime.time_left == 0:
# 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)
elif angle_to_player < -tolerance:
lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED)
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 = player_model.get_node(@"Skeleton/GunBone/ShootFrom/ShootParticle")
shoot_particle.restart()
shoot_particle.emitting = true
var muzzle_particle = player_model.get_node(@"Skeleton/GunBone/ShootFrom/MuzzleFlash")
muzzle_particle.restart()
muzzle_particle.emitting = true
fire_cooldown.start()
sound_effect_shoot.play()
else:
if $ReactionTime.is_paused():
$ReactionTime.set_paused(false) #restart reaction time
else:
# Player not in sight.
shoot_countdown = SHOOT_WAIT
if $ReactionTime.time_left != reaction_time:
$ReactionTime.start()
$ReactionTime.set_paused(true)
# 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)
elif angle_to_player < -tolerance:
lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED)
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 = animation_tree.get_root_motion_transform()
velocity = move_and_slide(root_motion.origin.length() * self.transform.basis.z * Vector3(1,0,1), Vector3.UP)
self.global_transform.origin += velocity
#self.global_transform.origin += root_motion.origin.length() * self.transform.basis.z * Vector3(1,0,1)
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()
# If shot at, attempt to look for player. This will extrapolate from impact position
# so they may validly look the wrong direction
if player == null or player == dummy:
player = dummy
last_player_sighting = (col.position - self.global_transform.origin) * 20 + self.global_transform.origin
# Enable longer focus detection shape
sight_focused.disabled = false
func _on_PlayerDetection1_body_entered(body):
if body.name == "Player" or body.name == "Target":
in_detection_normal = true
player = body
# Enable longer focus detection shape
sight_focused.disabled = false
func _on_PlayerDetection1_body_exited(body):
if body.name == "Player":
player = null
sight_focused.disabled = true