Skip to content

Commit

Permalink
Add support for networked multiplayer (godotengine#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
Faless authored Mar 6, 2023
1 parent 2beb6c4 commit bb833aa
Show file tree
Hide file tree
Showing 15 changed files with 796 additions and 355 deletions.
65 changes: 39 additions & 26 deletions enemies/red_robot/parts/part.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,54 @@ extends RigidBody3D

var puff_effect = preload("res://enemies/red_robot/parts/part_disappear_effect/part_disappear.tscn")

var _mat : Material = null

@export var lifetime: float = 3.0
@export var lifetime_random: float = 3.0
@export var disappearing_time: float = 0.5
@export var fade_value : float = 0.0 :
set(value):
fade_value = value
if _mat:
_mat.next_pass.set_shader_parameter("emission_cutout", fade_value)

var _lifetime
var _disappearing_counter = 0.0
var _is_disappearing = false
var _mat

@onready var _mesh = $Model.get_child(0)


func _ready():
_mat = _mesh.mesh.surface_get_material(0).duplicate()
_mesh.mesh.surface_set_material(0, _mat)
_mat.next_pass = _mat.next_pass.duplicate()
randomize()
_lifetime = lifetime + lifetime_random * randf()


func start_disappear_countdown():
await get_tree().create_timer(_lifetime).timeout
_is_disappearing = true
set_process(false)
if not OS.has_feature("dedicated_server"):
var mesh := $Model.get_child(0) as MeshInstance3D
_mat = mesh.mesh.surface_get_material(0).duplicate()
mesh.mesh.surface_set_material(0, _mat)
_mat.next_pass = _mat.next_pass.duplicate()


func explode():
# Start synching.
$MultiplayerSynchronizer.public_visibility = true
freeze = false
if not multiplayer.is_server():
return
get_node("Col1").disabled = false
get_node("Col2").disabled = false
linear_velocity = 3 * (Vector3.UP).normalized()
angular_velocity = (Vector3(randf(), randf(), randf()).normalized() * 2 - Vector3.ONE) * 10
await get_tree().create_timer(lifetime + lifetime_random * randf()).timeout
set_process(true)


func _process(delta):
if not _is_disappearing:
return
var curve_val = pow(_disappearing_counter / disappearing_time, 2.0)
_mat.next_pass.set_shader_parameter("emission_cutout", curve_val)
fade_value = pow(_disappearing_counter / disappearing_time, 2.0)
_disappearing_counter += delta
if _disappearing_counter >= disappearing_time - 0.2:
var puff = puff_effect.instantiate()
get_parent().add_child(puff)
puff.global_transform.origin = global_transform.origin
_is_disappearing = false
await get_tree().create_timer(0.2).timeout
queue_free()
destroy.rpc()
set_process(false)


@rpc("call_local")
func destroy():
var puff = puff_effect.instantiate()
get_parent().add_child(puff)
puff.global_transform.origin = global_transform.origin
await get_tree().create_timer(0.2).timeout
queue_free()
143 changes: 81 additions & 62 deletions enemies/red_robot/red_robot.gd
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ enum State {
SHOOTING = 2,
}

const PLAYER_AIM_TOLERANCE_DEGREES = 15
const PLAYER_AIM_TOLERANCE_DEGREES = deg_to_rad(15)

const SHOOT_WAIT = 6.0
const AIM_TIME = 1

const AIM_PREPARE_TIME = 0.5
const BLEND_AIM_SPEED = 0.05

@export var health: int = 5
signal exploded()

@export var test_shoot: bool = false

var state = State.APPROACH
@export var target_position := Vector3()
@export var health: int = 5
@export var state : State = State.APPROACH
@export var dead = false
@export var aim_preparing = AIM_PREPARE_TIME

var shoot_countdown = SHOOT_WAIT
var aim_countdown = AIM_TIME
var aim_preparing = AIM_PREPARE_TIME
var dead = false

var player = null
var orientation = Transform3D()
Expand Down Expand Up @@ -56,15 +59,21 @@ func _ready():
$AnimationTree.active = true
if test_shoot:
shoot_countdown = 0.0
animation_tree["parameters/state/transition_request"] = "idle" # Go idle.

if dead:
model.visible = false
collision_shape.disabled = true
animation_tree.active = false

animate()

func resume_approach():
state = State.APPROACH
aim_preparing = AIM_PREPARE_TIME
shoot_countdown = SHOOT_WAIT


@rpc("call_local")
func hit():
if dead:
return
Expand All @@ -74,39 +83,24 @@ func hit():
health -= 1
if health == 0:
dead = true
var base_xf = global_transform.basis
animation_tree.active = false
model.visible = false
death.visible = true
collision_shape.disabled = true

death_shield1.freeze = false
death_shield1.get_node("Col1").disabled = false
death_shield1.get_node("Col2").disabled = false

death_shield2.freeze = false
death_shield2.get_node("Col1").disabled = false
death_shield2.get_node("Col2").disabled = false

death_head.freeze = false
death_head.get_node("Col1").disabled = false
death_head.get_node("Col2").disabled = false

death_detach_spark1.emitting = true
death_detach_spark2.emitting = true

death_shield1.linear_velocity = 3 * (Vector3.UP - base_xf.x).normalized()
death_shield2.linear_velocity = 3 * (Vector3.UP + base_xf.x).normalized()
death_head.linear_velocity = 3 * (Vector3.UP).normalized()
death_shield1.angular_velocity = (Vector3(randf(), randf(), randf()).normalized() * 2 - Vector3.ONE) * 10
death_shield2.angular_velocity = (Vector3(randf(), randf(), randf()).normalized() * 2 - Vector3.ONE) * 10
death_head.angular_velocity = (Vector3(randf(), randf(), randf()).normalized() * 2 - Vector3.ONE) * 10

death_shield1.start_disappear_countdown()
death_shield2.start_disappear_countdown()
death_head.start_disappear_countdown()
death_shield1.explode()
death_shield2.explode()
death_head.explode()

explosion_sound.play()
exploded.emit()

if multiplayer.is_server():
await get_tree().create_timer(10.0).timeout
queue_free()


func shoot():
Expand Down Expand Up @@ -136,38 +130,73 @@ func shoot():
player.add_camera_shake_trauma(13)


func animate(delta:=0.0):
if state == State.APPROACH:
var to_player_local = target_position * global_transform
# The front of the robot 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)
if angle_to_player > PLAYER_AIM_TOLERANCE_DEGREES:
animation_tree["parameters/state/transition_request"] = "turn_left"
elif angle_to_player < -PLAYER_AIM_TOLERANCE_DEGREES:
animation_tree["parameters/state/transition_request"] = "turn_right"
elif target_position == Vector3.ZERO:
animation_tree["parameters/state/transition_request"] = "idle"
else:
animation_tree["parameters/state/transition_request"] = "walk"
else:
animation_tree["parameters/state/transition_request"] = "idle"

# Aiming or shooting
if target_position != Vector3.ZERO:
animation_tree["parameters/aiming/blend_amount"] = clamp(aim_preparing / AIM_PREPARE_TIME, 0, 1)

var to_cannon_local = target_position + Vector3.UP * ray_mesh.global_transform
var h_angle = rad_to_deg(atan2( to_cannon_local.x, -to_cannon_local.z ))
var v_angle = rad_to_deg(atan2( to_cannon_local.y, -to_cannon_local.z ))
var blend_pos = animation_tree.get("parameters/aim/blend_position")
var h_motion = BLEND_AIM_SPEED * delta * -h_angle
blend_pos.x += h_motion
blend_pos.x = clamp(blend_pos.x, -1, 1)

var v_motion = BLEND_AIM_SPEED * delta * v_angle
blend_pos.y += v_motion
blend_pos.y = clamp(blend_pos.y, -1, 1)

animation_tree["parameters/aim/blend_position"] = blend_pos


func _physics_process(delta):
if dead:
return

if not multiplayer.is_server():
animate(delta)
return

if test_shoot:
shoot()
test_shoot = false

if dead:
return

if not player:
animation_tree["parameters/state/transition_request"] = "idle" # Go idle.
target_position = Vector3()
animate(delta)
set_velocity(gravity * delta)
set_up_direction(Vector3.UP)
move_and_slide()
return

target_position = player.global_transform.origin

if state == State.APPROACH:
if aim_preparing > 0:
aim_preparing -= delta
if aim_preparing < 0:
aim_preparing = 0
animation_tree["parameters/aiming/blend_amount"] = aim_preparing / AIM_PREPARE_TIME

var to_player_local = player.global_transform.origin * global_transform
var to_player_local = target_position * global_transform
# The front of the robot 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 = deg_to_rad(PLAYER_AIM_TOLERANCE_DEGREES)
if angle_to_player > tolerance:
animation_tree["parameters/state/transition_request"] = "turn_left"
elif angle_to_player < -tolerance:
animation_tree["parameters/state/transition_request"] = "turn_right"
else:
animation_tree["parameters/state/transition_request"] = "walk"
if angle_to_player > -PLAYER_AIM_TOLERANCE_DEGREES and angle_to_player < PLAYER_AIM_TOLERANCE_DEGREES:
# Facing player, try to shoot.
shoot_countdown -= delta
if shoot_countdown < 0:
Expand All @@ -180,7 +209,6 @@ func _physics_process(delta):
state = State.AIM
aim_countdown = AIM_TIME
aim_preparing = 0
animation_tree["parameters/state/transition_request"] = "idle"
else:
# Player not in sight, do nothing.
shoot_countdown = SHOOT_WAIT
Expand All @@ -195,34 +223,19 @@ func _physics_process(delta):
if aim_preparing > AIM_PREPARE_TIME:
aim_preparing = AIM_PREPARE_TIME

animation_tree["parameters/aiming/blend_amount"] = clamp(aim_preparing / AIM_PREPARE_TIME, 0, 1)
aim_countdown -= delta
if aim_countdown < 0 and state == State.AIM:
var ray_origin = ray_from.global_transform.origin
var ray_to = player.global_transform.origin + Vector3.UP # Above middle of player.
var ray_to = target_position + Vector3.UP
var col = get_world_3d().direct_space_state.intersect_ray(PhysicsRayQueryParameters3D.create(ray_origin, ray_to, 0xFFFFFFFF, [self]))
if not col.is_empty() and col.collider == player:
state = State.SHOOTING
shoot_animation.play("shoot")
shoot_countdown = SHOOT_WAIT
play_shoot.rpc()
else:
resume_approach()

if animation_tree.active:
var to_cannon_local = player.global_transform.origin + Vector3.UP * ray_mesh.global_transform
var h_angle = rad_to_deg(atan2( to_cannon_local.x, -to_cannon_local.z ))
var v_angle = rad_to_deg(atan2( to_cannon_local.y, -to_cannon_local.z ))
var blend_pos = animation_tree.get("parameters/aim/blend_position")
var h_motion = BLEND_AIM_SPEED * delta * -h_angle
blend_pos.x += h_motion
blend_pos.x = clamp(blend_pos.x, -1, 1)

var v_motion = BLEND_AIM_SPEED * delta * v_angle
blend_pos.y += v_motion
blend_pos.y = clamp(blend_pos.y, -1, 1)

animation_tree["parameters/aim/blend_position"] = blend_pos

animate(delta)
# Apply root motion to orientation.
orientation *= Transform3D(animation_tree.get_root_motion_rotation(), animation_tree.get_root_motion_position())

Expand All @@ -240,13 +253,19 @@ func _physics_process(delta):
global_transform.basis = orientation.basis


@rpc("call_local")
func play_shoot():
shoot_animation.play("shoot")


func shoot_check():
test_shoot = true


func _clip_ray(length):
var mesh_offset = ray_mesh.position.z
ray_mesh.get_surface_override_material(0).set_shader_parameter("clip", length + mesh_offset)
if not OS.has_feature("dedicated_server"):
ray_mesh.get_surface_override_material(0).set_shader_parameter("clip", length + mesh_offset)


func _on_area_body_entered(body):
Expand Down
Loading

0 comments on commit bb833aa

Please sign in to comment.