Compare commits

...

6 Commits

Author SHA1 Message Date
22b6023849 Code cleanup 2022-11-16 23:13:33 +01:00
d8c60fbf41 Add water in a slightly hacky way 2021-06-28 20:00:10 +02:00
24ccf0ddbe Minor code cleanup 2021-06-28 12:39:56 +02:00
7a96f549ff Make ShootyEnemy cycle between targets by velocity 2021-06-28 12:00:46 +02:00
3bb4ed31e6 Add switch to velocity-based movement 2021-06-24 13:48:28 +02:00
15b8a20493 Add joystick input 2021-06-24 13:34:25 +02:00
16 changed files with 509 additions and 47 deletions

View File

@ -6,21 +6,14 @@ var target := Vector3.ZERO
var acceleration := Vector3.ZERO
var velocity := Vector3.ZERO
export var acceleration_factor = 5.0
export var accelerating := false
export var coefficient_of_restitution := 0.5
func _physics_process(delta):
if accelerating:
var direction = (target - global_transform.origin).normalized()
acceleration += direction * acceleration_factor * delta
velocity += acceleration
var collision = move_and_collide(velocity * delta)
if collision:
# Reflect the bullet and decrease the velocity according to the coefficient of restitution
var normal = collision.normal
velocity = coefficient_of_restitution * (velocity - 2 * velocity.dot(normal) * normal)

40
DeadzoneInput.gd Normal file
View File

@ -0,0 +1,40 @@
extends Object
class_name DeadzoneInput
static func get_input(type, outer_deadzone, inner_deadzone, min_length = 0.0, normalized = true):
var input = Vector2(Input.get_action_strength(type + "_up") -
Input.get_action_strength(type + "_down"),
Input.get_action_strength(type + "_right") -
Input.get_action_strength(type + "_left"))
# Remove signs to reduce the number of cases
var signs = Vector2(sign(input.x), sign(input.y))
input = Vector2(abs(input.x), abs(input.y))
if input.length() < min_length:
return Vector2.ZERO
# Deazones for each axis
if input.x > outer_deadzone:
input.x = 1.0
elif input.x < inner_deadzone:
input.x = 0.0
else:
input.x = inverse_lerp(inner_deadzone, outer_deadzone, input.x)
if input.y > outer_deadzone:
input.y = 1.0
elif input.y < inner_deadzone:
input.y = 0.0
else:
input.y = inverse_lerp(inner_deadzone, outer_deadzone, input.y)
# Re-apply signs
input *= signs
# Limit at length 1
if normalized and input.length() > 1.0:
input /= input.length()
return input

39
Orbiter.gd Normal file
View File

@ -0,0 +1,39 @@
extends KinematicBody
# Orbits the parent planet by velocity.
var time_passed := 0.0
var velocity := Vector3.ZERO
export(float) var move_speed_factor = 1.0
export(NodePath) var solar_system
func _ready():
var velocity_value = get_node(solar_system).get_orbit_velocity(global_transform.origin, get_parent())
# We need a vector perpendicular to the gravity acceleration to apply the velocity along.
# So we get the cross product of that and some arbitrary other vector, in this case Vector3.RIGHT
var gravity_acceleration = get_node(solar_system).get_closest_gravity_acceleration(global_transform.origin)
var other = Vector3.RIGHT
# Apply the velocity
velocity = gravity_acceleration.cross(other).normalized() * velocity_value
func _process(delta):
# Apply gravity as acceleration to velocity, then velocity to position
var gravity_acceleration = get_node(solar_system).get_closest_gravity_acceleration(global_transform.origin)
velocity += gravity_acceleration * delta
move_and_slide(velocity)
# Rotate down vector to face center of gravity -> tidally locked
var down = gravity_acceleration
var local_down = transform.basis * Vector3.DOWN
var angle = local_down.angle_to(down)
var axis = local_down.cross(down).normalized()
if axis != Vector3.ZERO: # Happens if we're perfectly aligned already (local_down and down are equal)
rotate(axis, angle)

View File

@ -21,14 +21,17 @@ func get_closest_gravity_acceleration(position: Vector3) -> Vector3:
var closest_planet_distance = INF
var closest_force = 0.0
# Iterate through all planets to find the closest one
for planet in get_children():
var pos_to_center = (planet.transform.origin - position)
var distance = pos_to_center.length()
if distance < closest_planet_distance:
# This planet is closer than the previous ones -> calculate and save the values
var force = _gravity(planet.mass * mass_multiplier, distance * distance_multiplier)
force *= gravity_multiplier
# Multiply by the normalized vector towards the center to give the force a direction
closest_force = (pos_to_center / distance) * force
closest_planet_distance = distance
@ -39,6 +42,7 @@ func get_closest_gravity_acceleration(position: Vector3) -> Vector3:
func get_gravity_acceleration(position: Vector3) -> Vector3:
var total_force = Vector3.ZERO
# Add each planet's gravity force to the total force
for planet in get_children():
var pos_to_center = (planet.transform.origin - position)
var distance = pos_to_center.length()
@ -51,6 +55,18 @@ func get_gravity_acceleration(position: Vector3) -> Vector3:
return total_force
# Return the velocity needed to orbit the given planet
func get_orbit_velocity(position: Vector3, planet):
var pos_to_center = (planet.global_transform.origin - position)
var distance = pos_to_center.length()
# raw_velocity is the velocity according to the formula, but we also need to account for the gravity_multiplier.
var raw_velocity = -sqrt((G * planet.mass * mass_multiplier) / (distance * distance_multiplier))
var gravity_scale_factor = sqrt(gravity_multiplier / distance_multiplier)
return raw_velocity * gravity_scale_factor
# Formula for gravity
static func _gravity(mass, distance):
return (G * mass) / (distance * distance)

View File

@ -44,8 +44,7 @@ func get_center():
func apply_acceleration(acceleration):
# First drag, then add the new acceleration
# For drag: Lerp towards the target velocity
# This is usually 0, unless we're on something that's moving, in which case it is that object's
# velocity
# This is usually 0, unless we're on something that's moving, in which case it is that object's velocity
velocity = lerp(velocity, current_target_velocity, drag)
velocity += acceleration
@ -61,8 +60,7 @@ func get_gravity_acceleration():
var distance_to_collision = ($GroundCheckRay.get_collision_point()
- $GroundCheckRay.global_transform.origin).length()
# This factor is 0.0 if the player is exactly on the ground, and 1.0 if they're just barely
# almost grounded
# This factor is 0.0 if the player is exactly on the ground, and 1.0 if they're just barely almost grounded
var factor = inverse_lerp(0.0, almost_on_ground_length, distance_to_collision)
return lerp(planet_gravity, total_gravity, factor)
@ -111,24 +109,23 @@ func _physics_process(delta):
var move_velocity := Vector3.ZERO
var move_acceleration := Vector3.ZERO
# Movement and rotation
if Input.is_action_pressed("move_up"):
move_acceleration.z -= move_accel
if Input.is_action_pressed("move_down"):
move_acceleration.z += move_accel
if Input.is_action_pressed("move_left"):
rotate(transform.basis.y, delta * rotate_speed)
if Input.is_action_pressed("move_right"):
rotate(transform.basis.y, -delta * rotate_speed)
# Apply input
var input = DeadzoneInput.get_input("move", 0.65, 0.2, 0.0, false)
# Either do velocity-based movement or force-based movement
if Input.is_action_pressed("movement_modifier"):
move_velocity = transform.basis * Vector3.FORWARD * 15.0 * input.x
else:
move_acceleration += transform.basis * Vector3.FORWARD * move_accel * input.x
rotate(transform.basis.y, delta * rotate_speed * -input.y)
# Make movement local
move_acceleration = transform.basis * move_acceleration
move_acceleration = move_acceleration
# Get acceleration caused by gravity
var gravity_acceleration = get_gravity_acceleration()
# FIXME: Consider setting the gravity_acceleration to 0 if on ground
# Apply both acceleration types
apply_acceleration((move_acceleration + gravity_acceleration) * delta)
@ -150,7 +147,8 @@ func _physics_process(delta):
reset_moving_platform_velocity()
# Apply movement to position
velocity = move_and_slide(velocity)
# Add and subtract the move_velocity (used for velocity-based movement) because we do want to apply it, but not remember it for next frame
velocity = move_and_slide(velocity + move_velocity) - move_velocity
# Clamp the velocity just to be save
velocity.x = clamp(velocity.x, -MAX_VEL, MAX_VEL)

View File

@ -1,5 +1,8 @@
extends KinematicBody
# An enemy which shoots at the future player position and moves between two targets within a given
# amount of time.
export(NodePath) var player_node
onready var player = get_node(player_node)
@ -7,46 +10,83 @@ onready var player = get_node(player_node)
export(NodePath) var solar_system_node
onready var solar_system = get_node(solar_system_node)
export(Array, NodePath) var target_paths
export var target_fly_time := 5.0
var time_passed_since_fly_start := 0.0
var current_target_index = 0
var bullet_scene = preload("res://Bullet.tscn")
var bullet_velocity = 40.0
var target
var velocity := Vector3(0.0, 1.0, 1.0)
var shoot_target
var velocity := Vector3.ZERO
func _ready():
# Shoot everytime the timer times out
$ShotTimer.connect("timeout", self, "shoot_bullet")
# Set up pathing: Start at the first target
global_transform.origin = get_node(target_paths[0]).global_transform.origin
_set_velocity_to_fly_towards_next_target()
# Picks the next target position from the `target_paths` list and sets the `velocity` appropriately
# so that the target will be reached after `target_fly_time`.
func _set_velocity_to_fly_towards_next_target():
current_target_index = (current_target_index + 1) % target_paths.size()
# Vector from here to the new target
var vector_to_next = (get_node(target_paths[current_target_index]).global_transform.origin
- global_transform.origin)
# s = v * t --> v = s / t
velocity = vector_to_next / target_fly_time
time_passed_since_fly_start = 0.0
func _physics_process(delta):
# Project the player's position into the future
target = _get_future_position(
shoot_target = _get_future_position(
player.get_center(),
player.velocity - velocity
)
if target:
# Look at the target that will be shot at, with the gravity as the down vector
if shoot_target:
var gravity = solar_system.get_gravity_acceleration(global_transform.origin)
look_at(target, gravity)
look_at(shoot_target, gravity)
# Have we rearched the target?
time_passed_since_fly_start += delta
if time_passed_since_fly_start >= target_fly_time:
# If so, fly towards the next target
_set_velocity_to_fly_towards_next_target()
# Apply velocity
global_transform.origin += velocity * delta
func shoot_bullet():
if not target:
if not shoot_target:
# Player can't be hit right now, abort
return
# Add a bullet
var instance = bullet_scene.instance()
get_tree().get_root().add_child(instance)
# Make the bullet start at the current position of this object and fly towards the target.
# The own velocity is added because of galilean relativity.
instance.global_transform.origin = global_transform.origin - global_transform.basis.z
instance.velocity = velocity + (target - global_transform.origin).normalized() * bullet_velocity
instance.velocity = velocity + (shoot_target - global_transform.origin).normalized() * bullet_velocity
# Return the position at which a bullet fired forwards and with the velocity `bullet_velocity` would
# intersect with the object that is at `position` and moving with the given `velocity`
func _get_future_position(position, velocity):
# Solution to the quadratic formula gives us the time at which the player would be hit
# TODO: Take acceleration into account as well!
var a = pow(velocity.x, 2) + pow(velocity.y, 2) + pow(velocity.z, 2) - pow(bullet_velocity, 2)
var b = 2 * (velocity.x * (position.x - global_transform.origin.x)
+ velocity.y * (position.y - global_transform.origin.y)
@ -67,4 +107,5 @@ func _get_future_position(position, velocity):
var t = min(t1, t2)
if t < 0: t = max(t1, t2)
# Return the position given by the time we calculated
return position + t * velocity

26
World.gd Normal file
View File

@ -0,0 +1,26 @@
extends Spatial
const ray_length = 2000
func _physics_process(delta):
if Input.is_mouse_button_pressed(1):
# If the left mouse button was pressed, cast a ray towards the mouse position
var mouse_pos = get_viewport().get_mouse_position()
var camera = $LerpedFollow/LerpedCamera
var from = camera.project_ray_origin(mouse_pos)
var to = from + camera.project_ray_normal(mouse_pos) * ray_length
var result = get_world().direct_space_state.intersect_ray(from, to)
if not result.empty():
# If there was a collision, set the depth of the water at that position
$Water.set_depth_at_position(result.position, 0.0)
func _ready():
# Create some initial waves
for i in range(0, 11):
$Water.set_depth_at_position(Vector3(-50 + i * 10, -66, -50), 0.0)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://addons/interactive_water/WaterHeights.gd" type="Script" id=1]
[node name="PersistentShaderTexture" type="Node"]
script = ExtResource( 1 )
[node name="Viewport" type="Viewport" parent="."]
size = Vector2( 1, 1 )
transparent_bg = true
handle_input_locally = false
hdr = false
disable_3d = true
usage = 0
render_target_v_flip = true
render_target_update_mode = 3
[node name="Texture" type="ColorRect" parent="Viewport"]
margin_right = 128.0
margin_bottom = 128.0
rect_min_size = Vector2( 128, 128 )

View File

@ -0,0 +1,72 @@
extends Spatial
# Saves every frame as a PNG in the project directory. Use for debugging (with caution)
export var output_debug_textures := false
export var first_output_frame := 0
export var plane_size := 2.0
var _frame_number := 0
var _positions_to_set = []
func set_depth_at_position(pos: Vector3, depth: float):
# Transform the global position into 2D points on the water surface with values between 0 and 1
pos -= translation
var position_2d = Vector2(pos.x, pos.z)
position_2d += Vector2(plane_size, plane_size) / 2.0
position_2d /= plane_size
if position_2d.x < 1.0 and position_2d.y < 1.0 \
and position_2d.x >= 0.0 and position_2d.y >= 0.0:
_positions_to_set.append([position_2d, depth])
func _ready():
$WaterMesh.mesh.size = Vector2(plane_size, plane_size)
# Apply the scale and positioning to the collider
$WaterMesh/StaticBody.scale.x = plane_size / 2.0
$WaterMesh/StaticBody.scale.y = plane_size / 2.0
$WaterMesh/StaticBody.scale.z = plane_size / 2.0
$WaterMesh/StaticBody.translation.y = 0.6 * 40.0
func _physics_process(delta):
# Get result of previous frame
var result = $WaterHeights.get_texture()
# Set it as the data of the water mesh
$WaterMesh.material_override.set_shader_param("water_heights", result)
# Calculate a new frame. First, get the data of the last frame and modify it accordingly
var image_data = result.get_data()
# Set outstanding pixels (after some frames, since the texture isn't initialized until then)
if _frame_number >= 60:
for position_and_depth in _positions_to_set:
var pos = position_and_depth[0]
var depth = position_and_depth[1]
image_data.lock()
image_data.set_pixel(floor(pos.x * 512), floor(pos.y * 512), Color(depth, 0.0, 0.0, 0.0))
image_data.unlock()
_positions_to_set.clear()
# Create an ImageTexture for this new frame
var previous_frame = ImageTexture.new()
previous_frame.create_from_image(image_data)
# Set the previous texture in the shader so that a new texture will be available next frame
$WaterHeights.set_previous_texture(previous_frame)
# Debug output if needed
if output_debug_textures and _frame_number > first_output_frame:
image_data.save_png("res://debugframes/frame%s.png" % [_frame_number])
_frame_number += 1

View File

@ -0,0 +1,18 @@
shader_type spatial;
uniform sampler2D water_heights;
float read_height(sampler2D tex, vec2 uv) {
return texture(tex, uv).r + texture(tex, uv).g / 255.0;
}
void fragment() {
ALBEDO = vec3(0.5, 0.7, 1.0) * (read_height(water_heights, UV) - 0.6) * 3.0;
}
void vertex() {
float vertex_distance = length(VERTEX) * 0.06;
VERTEX += NORMAL * (read_height(water_heights, UV) * 40.0 - pow(2, vertex_distance));
}

View File

@ -0,0 +1,38 @@
[gd_scene load_steps=9 format=2]
[ext_resource path="res://addons/interactive_water/Water.shader" type="Shader" id=1]
[ext_resource path="res://addons/interactive_water/Water.gd" type="Script" id=3]
[ext_resource path="res://addons/interactive_water/WaterUpdateMaterial.tres" type="Material" id=4]
[ext_resource path="res://addons/interactive_water/PersistentShaderTexture.tscn" type="PackedScene" id=5]
[sub_resource type="ViewportTexture" id=1]
viewport_path = NodePath("WaterHeightViewport")
[sub_resource type="ShaderMaterial" id=2]
resource_local_to_scene = true
shader = ExtResource( 1 )
shader_param/water_heights = SubResource( 1 )
[sub_resource type="PlaneMesh" id=3]
subdivide_width = 512
subdivide_depth = 512
[sub_resource type="PlaneShape" id=4]
[node name="Water" type="Spatial"]
script = ExtResource( 3 )
[node name="WaterMesh" type="MeshInstance" parent="."]
material_override = SubResource( 2 )
mesh = SubResource( 3 )
material/0 = null
[node name="StaticBody" type="StaticBody" parent="WaterMesh"]
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0 )
[node name="CollisionShape" type="CollisionShape" parent="WaterMesh/StaticBody"]
shape = SubResource( 4 )
[node name="WaterHeights" parent="." instance=ExtResource( 5 )]
size = Vector2( 512, 512 )
shader_material = ExtResource( 4 )

View File

@ -0,0 +1,30 @@
extends Node
# A texture which is continuously (statefully) updated by a shader.
#
# Typical usage: Retrieve the current result with `get_texture` and, at the end of the frame,
# re-insert that texture (updated if needed) with `set_previous_texture`.
#
# Note that the given shader needs to accept a `previous_frame` sampler2D. This represents the
# result of the last frame which is used for generating the new result.
export var size := Vector2(64, 64)
export var shader_material: ShaderMaterial
func _ready():
$Viewport/Texture.rect_min_size = size
$Viewport/Texture.rect_size = size
$Viewport.size = size
assert(shader_material)
$Viewport/Texture.material = shader_material
func get_texture():
return $Viewport.get_texture()
func set_previous_texture(texture):
$Viewport/Texture.material.set_shader_param("previous_frame", texture)

View File

@ -0,0 +1,63 @@
shader_type canvas_item;
render_mode unshaded, blend_disabled;
uniform sampler2D previous_frame;
uniform float water_height = 0.6;
uniform float height_damping = 0.13;
uniform float velocity_damping = 0.8;
uniform float spread = 0.94;
// Height and Velocity are encoded in two components each, so RG is height and BA is velocity.
// This is needed to get a workable accuracy.
float read_height(sampler2D tex, vec2 uv) {
return texture(tex, uv).r + texture(tex, uv).g / 255.0;
}
float read_velocity(sampler2D tex, vec2 uv) {
return texture(tex, uv).b + texture(tex, uv).a / 255.0;
}
float get_encoded_remainder(float num) {
return fract(num * 255.0);
}
void fragment() {
// Read values here
float height_here = read_height(previous_frame, UV);
float velocity_here = read_velocity(previous_frame, UV);
// Apply force towards the base height
// This follows from the damped harmonic oscillator equation F = -kx-bv
float force = -height_damping * (height_here - water_height) - velocity_here * velocity_damping;
float acceleration_here = force;
// In addition to each individual height behaving like a spring, neighbouring heights are
// "connected by springs" as well:
// Read more samples
float uv_mod = 1.0 / float(textureSize(previous_frame, 0).x);
float height_up = read_height(previous_frame, UV + vec2(0.0, uv_mod));
float height_down = read_height(previous_frame, UV + vec2(0.0, -uv_mod));
float height_left = read_height(previous_frame, UV + vec2(-uv_mod, 0.0));
float height_right = read_height(previous_frame, UV + vec2(uv_mod, 0.0));
// Calculate differences
float up_delta = spread * (height_up - height_here);
float down_delta = spread * (height_down - height_here);
float left_delta = spread * (height_left - height_here);
float right_delta = spread * (height_right - height_here);
// Use the biggest delta to apply to this height
float sum_delta = max(max(left_delta, right_delta), max(up_delta, down_delta));
// Apply velocity and height
velocity_here += sum_delta + acceleration_here;
height_here += velocity_here;
// Write to the texture
COLOR = vec4(
height_here, get_encoded_remainder(height_here),
velocity_here, get_encoded_remainder(velocity_here)
);
}

View File

@ -0,0 +1,10 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=2]
[ext_resource path="res://addons/interactive_water/WaterUpdate.shader" type="Shader" id=1]
[resource]
shader = ExtResource( 1 )
shader_param/water_height = 0.6
shader_param/height_damping = 0.13
shader_param/velocity_damping = 0.8
shader_param/spread = 0.94

View File

@ -9,6 +9,11 @@
config_version=4
_global_script_classes=[ {
"base": "Object",
"class": "DeadzoneInput",
"language": "GDScript",
"path": "res://DeadzoneInput.gd"
}, {
"base": "KinematicBody",
"class": "MovingPlatform",
"language": "GDScript",
@ -25,6 +30,7 @@ _global_script_classes=[ {
"path": "res://Planets.gd"
} ]
_global_script_class_icons={
"DeadzoneInput": "",
"MovingPlatform": "",
"Planet": "",
"SolarSystem": ""
@ -63,28 +69,38 @@ ui_down={
]
}
move_up={
"deadzone": 0.5,
"deadzone": 0.0,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
]
}
move_down={
"deadzone": 0.5,
"deadzone": 0.0,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
]
}
move_left={
"deadzone": 0.5,
"deadzone": 0.0,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
]
}
move_right={
"deadzone": 0.5,
"deadzone": 0.0,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
]
}
jump={
"deadzone": 0.5,
"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
]
}
movement_modifier={
"deadzone": 0.5,
"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
]
}