Last active
January 22, 2024 23:09
-
-
Save DearthDev/cd26f48351988c4e61bfcece27c49d68 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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