Skip to content

Instantly share code, notes, and snippets.

@DearthDev
Last active January 22, 2024 23:09
Show Gist options
  • Save DearthDev/cd26f48351988c4e61bfcece27c49d68 to your computer and use it in GitHub Desktop.
Save DearthDev/cd26f48351988c4e61bfcece27c49d68 to your computer and use it in GitHub Desktop.
# This file contains a series of functions used to control a volleyball for a
# beach volleyball game. The game's design required a solution that allowed
# accuracy and strength be different for each character. The scripts rely on
# algebra to meet the games design requirements and allow animations to line
# up with the ball when being hit.
# The player or AI aims where they want to hit the ball and this function finds
# how best to hit the ball to reach that part of the court.
# Hitting the ball at the lowest posible angle that still clears the net makes
# for the least air time and least time for opponents to get in position.
# When hitting the ball form above then net like in spiking the ball can be hit
# at downward angles.
# Hits the ball to the other side of the net based on aim, accuracy and power.
func attack(aim_vector: Vector2, accuracy: float, max_power: float, to_far_side: bool) -> void:
var target_position := Vector3()
if far_side:
target_position.z = GameManager.COURT_HALF_LENGTH
else:
target_position.z = -GameManager.COURT_HALF_LENGTH
# Offset the target position based on the aim vector.
target_position.x += aim_vector.x * GameManager.COURT_HALF_WIDTH * 0.5
target_position.z += aim_vector.y * GameManager.COURT_HALF_LENGTH * 0.5
# Randomize the target position based on the accuracy.
target_position.x += accuracy - accuracy * 2.0 * randf()
target_position.z += accuracy - accuracy * 2.0 * randf()
# Find the minimum angle and power that the ball can be launched at that clears the
# net, hits the target position and requires less than the athlete's maximum power.
var pitch := deg_to_rad(70.0)
var power := max_power
for angle in range(-85, 90, 15):
var p := _get_attack_power(target_position, deg_to_rad(angle))
if p < max_power:
power = p
pitch = deg_to_rad(angle)
break
_launch(target_position, pitch, power, accuracy)
touches = 0
# In beach volleyball, opponents spike the ball as hard as they can across then net.
# Then, athelets "recive" the ball. Since the ball is traveling so fast athletes
# try to hit the ball towards their teammate who will have an easier time
# positioning the ball for a good counter offense.
# The game design required a function to hit the ball towards the teammate
# prioritizing a high arcing hit to allow more time for their teammate to get
# into position.
# Another requirement was that fast moving balls tend to keep a portion of their
# velocity after being "recived" and may fly far into the back court.
# Hits the ball towards their teammate based on accuracy.
func recive(teammate_position: Vector3, accuracy: float) -> void:
# Find the position the ball would land if it simply bounced off the athlete's
# hands without imparting any directionality or velocity.
velocity.y = -velocity.y
var t := time_to_height(0.0)
var bounce_position := Vector3()
bounce_position.x = global_transform.origin.x + velocity.x * t
bounce_position.z = global_transform.origin.z + velocity.z * t
var target_position:Vector3 = lerp(bounce_position, teammate_position, 0.4)
var pitch := _get_pass_angle(target_position, PASS_POWER)
_launch(target_position, pitch, PASS_POWER, accuracy)
touches += 1
# Will return a negative number or negative infinity if the ball is
# already lower than the height.
func time_to_height(height: float) -> float:
# Since the ball follows a parabolic curve there are two times when
# the ball will be the right height. One is in the future and one is in
# the past. Use only the positive part of the quadratic formula.
# d = v * t - 0.5 * g * t^2
# 0 = 0.5 * g * t^2 - v * t - d
var a := 0.5 * g
var b := -velocity.y
var c := height - global_transform.origin.y
var s := b * b - 4.0 * a * c
if s < 0.0:
return -INF
return (-b + sqrt(s)) / (2 * a)
# Helper function to get the 2D position of the ball after the given seconds have passed.
func v2pos_at(time: float) -> Vector2:
return Vector2(global_transform.origin.x + velocity.x * time, global_transform.origin.z + velocity.z * time)
func _get_pass_angle(target_position: Vector3, power: float) -> float:
# Use projectile motion physics to calculate what angle to launch the ball at.
# Because there is two angles that produce the desired result, the equation is
# going to be quadratic.
# y = y0 + v * sin(angle) * t - 0.5 * g * t^2
# x = v * cos(angle) * t
# x is the lateral distance to the target and the equation can be solved for t.
# t = x / v / cos(angle)
# Plug t in the the first equation and simplify.
# 0 = y0 + (v * sin(angle)) * (x / v / cos(angle)) - 0.5 * g * (x / v / cos(angle))^2
# 0 = y0 + (v * sin(angle) * x / v) / cos(angle) - 0.5 * g * x^2 / v^2 / cos(angle)^2
# 0 = y0 + (v * sin(angle) * x / v) / cos(angle) - 0.5 * g * x^2 / v^2 * 1.0 / cos(angle)^2
# sin(x)/cos(x) = tan(x) ^^^ ^^^
# 1.0 / cos(x)^2 = sec(x)^2 = 1 + tan(x)^2 ^^^
# 0 = y0 + v * x / v * tan(angle) - 0.5 * g * x^2 / v^2 * (1.0 + tan(angle)^2)
# 0 = y0 + x * tan(angle) - 0.5 * g * x^2 / v^2 * (1.0 + tan(angle)^2)
# 0 = y0 + x * tan(angle) - (0.5 * g * x^2 * (1.0 + tan(angle)^2)) / v^2
# 0 = y0 + x * tan(angle) - (0.5 * g * x^2 * tan(angle)^2 + 0.5 * g * x^2) / v^2
# 0 = y0 * v^2 + x * tan(angle) * v^2 - 0.5 * g * x^2 * tan(angle)^2 - 0.5 * g * x^2
# 0 = -0.5 * g * x^2 * tan(angle)^2 + v^2 * x * tan(angle) - 0.5 * g * x^2 + v^2 * y
# Finally, we have a quadratic with tan(angle) and tan(angle)^2.
# let z = tan(angle)
# Plug it into the quadratic equation.
# z = (-b +- sqrt(b^2 - 4 * a * c)) / (2 * a)
# a = -0.5 * g * x^2
# b = v^2 * x
# c = -0.5 * g * x^2 + v^2 * y
var y0 := global_transform.origin.y
var x := Vector2(target_position.x - global_transform.origin.x, target_position.z - global_transform.origin.z).length()
var v := power
var x2 := x * x
var v2 := v * v
var a := -0.5 * g * x2
var b := v2 * x
var c := -0.5 * g * x2 + v2 * y0
var s := b * b - 4.0 * a * c
if s < 0.0:
# Not powerfull enough to reach the target. Launch at 45 degrees for maxium distance.
return deg_to_rad(45.0)
# Only use the higher of the two angles to allow more time for teammate to get into position.
var d := 2 * a
var z2 := (-b - sqrt(s)) / d
return atan(z2)
# Will return infinity if it's impossible to reach the target.
# Angle has to be in the range: -90 < angle < 90.
func _get_attack_power(target_position: Vector3, angle: float) -> float:
# Use projectile motion physics to calculate what speed to launch the ball at.
# y = y0 + v * sin(angle) * t - 0.5 * g * t^2
# x = v * cos(angle) * t
# Solve the second equation for t
# t = x / (v * cos(angle))
# Plug t into the first equation.
# 0 = y0 + v * sin(angle) * (x / (v * cos(angle))) - 0.5 * g * (x / (v * cos(angle)))^2
# sin(x)/cos(x) = tan(x)
# 0 = y0 + v * x * tan(angle) - 0.5 * (g * x^2) / (v^2 * cos(angle)^2)
# (g * x^2) / (2.0 * v^2 * cos(angle)^2) = y0 + x * tan(angle)
# v^2 = (g * x^2) / (2 * cos(angle)^2 * (y0 + x * tan(angle)))
# v = sqrt((g * x^2) / (2 * cos(angle)^2 * (y0 + x * tan(angle))))
assert(angle > -90 and angle < 90)
var y0 := global_transform.origin.y
var diff := Vector2(target_position.x - global_transform.origin.x, target_position.z - global_transform.origin.z)
var x := diff.length()
var cos_angle := cos(angle)
var s := (g * x * x) / (2.0 * cos_angle * cos_angle * (y0 + x * tan(angle)))
if s < 0.0:
return INF
var power := sqrt(s)
# Check if ball will pass over the net.
var cross_x := Vector2(diff.x * global_transform.origin.z / diff.y, global_transform.origin.z).length()
var t := cross_x / (power * cos(angle))
var height := y0 + power * sin(angle) * t - 0.5 * g * t * t
if height < MIN_HEIGHT_AT_NET:
return INF
return power
# Helper function to set the velocity of the ball.
func _launch(target_position: Vector3, pitch: float, power: float, accuracy: float) -> void:
var dir := Vector2(target_position.x - global_transform.origin.x, target_position.z - global_transform.origin.z).normalized()
var yaw := atan2(dir.x, dir.y)
yaw += accuracy - accuracy * 2.0 * randf()
pitch += accuracy * 0.5 - accuracy * randf()
velocity = (Quaternion(Vector3.UP, yaw) * Quaternion(Vector3.RIGHT, -pitch)) * (-Vector3.FORWARD * power)
time_to_ground = time_to_height(0.0)
ground_hit_position = v2pos_at(time_to_ground)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment