diff --git a/project/box_physics.gd b/project/box_physics.gd new file mode 100644 index 0000000..a14f813 --- /dev/null +++ b/project/box_physics.gd @@ -0,0 +1,121 @@ + +const EPSILON = 0.001 + +# Gets the transformed vector for moving a box and slide. +# This algorithm is free from tunnelling for axis-aligned movement, +# except in some high-speed diagonal cases or huge size differences: +# For example, if a box is fast enough to have a diagonal motion jumping from A to B, +# it will pass through C if that other box is the only other one: +# +# o---o +# | A | +# o---o +# o---o +# | C | +# o---o +# o---o +# | B | +# o---o +# +# TODO one way to fix this would be to try a "hot side" projection instead +# +static func get_motion(box, motion, other_boxes): + # The bounding box is expanded to include it's estimated version at next update. + # This also makes the algorithm tunnelling-free + var expanded_box = expand_with_vector(box, motion) + + var colliding_boxes = [] + for other in other_boxes: + if expanded_box.intersects(other): + colliding_boxes.append(other) + + if colliding_boxes.size() == 0: + return motion + #print("Colliding: ", colliding_boxes.size()) + + var new_motion = motion + + for other in colliding_boxes: + new_motion.y = calculate_y_offset(other, box, new_motion.y) + box.position.y += new_motion.y + + for other in colliding_boxes: + new_motion.x = calculate_x_offset(other, box, new_motion.x) + box.position.x += new_motion.x + + for other in colliding_boxes: + new_motion.z = calculate_z_offset(other, box, new_motion.z) + box.position.z += new_motion.z + + return new_motion + + +static func expand_with_vector(box, v): + if v.x > 0: + box.size.x += v.x + elif v.x < 0: + box.position.x += v.x + box.size.x -= v.x + if v.y > 0: + box.size.y += v.y + elif v.y < 0: + box.position.y += v.y + box.size.y -= v.y + if v.z > 0: + box.size.z += v.z + elif v.z < 0: + box.position.z += v.z + box.size.z -= v.z + return box + + +static func calculate_z_offset(box, other, motion_z): + if other.end.y <= box.position.y || other.position.y >= box.end.y: + return motion_z + if other.end.x <= box.position.x || other.position.x >= box.end.x: + return motion_z + if motion_z > 0.0 and other.end.z <= box.position.z: + var off = box.position.z - other.end.z - EPSILON + if off < motion_z: + motion_z = off + if motion_z < 0.0 and other.position.z >= box.end.z: + var off = box.end.z - other.position.z + EPSILON + if off > motion_z: + motion_z = off + return motion_z + + +static func calculate_x_offset(box, other, motion_x): + if other.end.z <= box.position.z || other.position.z >= box.end.z: + return motion_x + if other.end.y <= box.position.y || other.position.y >= box.end.y: + return motion_x + if motion_x > 0.0 and other.end.x <= box.position.x: + var off = box.position.x - other.end.x - EPSILON + if off < motion_x: + motion_x = off + if motion_x < 0.0 and other.position.x >= box.end.x: + var off = box.end.x - other.position.x + EPSILON + if off > motion_x: + motion_x = off + return motion_x + + +static func calculate_y_offset(box, other, motion_y): + if other.end.z <= box.position.z || other.position.z >= box.end.z: + return motion_y + if other.end.x <= box.position.x || other.position.x >= box.end.x: + return motion_y + if motion_y > 0.0 and other.end.y <= box.position.y: + var off = box.position.y - other.end.y - EPSILON + if off < motion_y: + motion_y = off + if motion_y < 0.0 and other.position.y >= box.end.y: + var off = box.end.y - other.position.y + EPSILON + if off > motion_y: + motion_y = off + return motion_y + + +static func box_from_center_extents(center, extents): + return Rect3(center - extents, 2.0*extents) diff --git a/project/character_avatar.tscn b/project/character_avatar.tscn index 82051ef..1f13f59 100644 --- a/project/character_avatar.tscn +++ b/project/character_avatar.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=8 format=2] +[gd_scene load_steps=9 format=2] [ext_resource path="res://character_controller.gd" type="Script" id=1] [ext_resource path="res://mouse_look.gd" type="Script" id=2] @@ -15,56 +15,64 @@ height = 0.85 flags_transparent = false flags_unshaded = false +flags_vertex_lighting = false flags_on_top = false flags_use_point_size = false flags_fixed_size = false vertex_color_use_as_albedo = true vertex_color_is_srgb = false params_diffuse_mode = 0 +params_specular_mode = 0 params_blend_mode = 0 params_cull_mode = 0 params_depth_draw_mode = 0 params_line_width = 2.0 params_point_size = 1.0 params_billboard_mode = 0 +params_grow = false +params_use_alpha_scissor = false albedo_color = Color( 1, 1, 1, 1 ) -specular_mode = 0 -specular_color = Color( 0.1, 0.1, 0.1, 1 ) -specular_metalness = 0.1 -specular_roughness = 0.0 +metallic = 0.0 +metallic_specular = 0.5 +metallic_texture_channel = 0 +roughness = 0.0 +roughness_texture_channel = 0 emission_enabled = false normal_enabled = false rim_enabled = false clearcoat_enabled = false anisotropy_enabled = false ao_enabled = false -height_enabled = false +depth_enabled = false subsurf_scatter_enabled = false refraction_enabled = false detail_enabled = false -uv1_scale = Vector2( 1, 1 ) -uv1_offset = Vector2( 0, 0 ) -uv2_scale = Vector2( 1, 1 ) -uv2_offset = Vector2( 0, 0 ) +uv1_scale = Vector3( 1, 1, 1 ) +uv1_offset = Vector3( 0, 0, 0 ) +uv1_triplanar = false +uv1_triplanar_sharpness = 1.0 +uv2_scale = Vector3( 1, 1, 1 ) +uv2_offset = Vector3( 0, 0, 0 ) +uv2_triplanar = false +uv2_triplanar_sharpness = 1.0 + +[sub_resource type="CubeMesh" id=3] + +size = Vector3( 0.8, 1.8, 0.8 ) +subdivide_width = 0 +subdivide_height = 0 +subdivide_depth = 0 [node name="CharacterAvatar" type="KinematicBody"] input_ray_pickable = true input_capture_on_drag = false -shape_count = 1 -shapes/0/shape = SubResource( 1 ) -shapes/0/transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ) -shapes/0/trigger = false collision_layer = 3 collision_mask = 1 -collide_with/static = true -collide_with/kinematic = true -collide_with/rigid = true -collide_with/character = true -collision/margin = 0.001 +collision/safe_margin = 0.001 script = ExtResource( 1 ) speed = 5.0 -gravity = 9.8 +gravity = 40.0 jump_force = 8.0 head = NodePath("Camera") terrain = null @@ -73,8 +81,7 @@ terrain = null transform = Transform( 1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0 ) shape = SubResource( 1 ) -trigger = false -_update_shape_index = 0 +disabled = false [node name="Camera" type="Camera" parent="."] @@ -89,11 +96,14 @@ cull_mask = 1048575 environment = null h_offset = 0.0 v_offset = 0.0 +doppler/tracking = 0 script = ExtResource( 2 ) +_sections_unfolded = [ "Transform" ] sensitivity = 0.3 min_angle = -90 max_angle = 90 capture_mouse = true +distance = 4.0 [node name="interaction" type="Node" parent="."] @@ -103,10 +113,16 @@ cursor_material = SubResource( 2 ) [node name="debug_label" type="Label" parent="."] +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 margin_right = 379.0 margin_bottom = 325.0 +rect_pivot_offset = Vector2( 0, 0 ) rect_clip_content = false mouse_filter = 2 +size_flags_horizontal = 1 size_flags_vertical = 0 percent_visible = 1.0 lines_skipped = 0 @@ -131,10 +147,26 @@ shadow_color = Color( 0, 0, 0, 1 ) shadow_bias = 0.1 shadow_contact = 0.0 shadow_max_distance = 0.0 +shadow_reverse_cull_face = false editor_only = false omni_range = 10.0 omni_attenuation = 2.54912 omni_shadow_mode = 1 omni_shadow_detail = 1 +[node name="MeshInstance" type="MeshInstance" parent="."] + +layers = 1 +material_override = null +cast_shadow = 1 +extra_cull_margin = 0.0 +use_in_baked_light = false +lod_min_distance = 0.0 +lod_min_hysteresis = 0.0 +lod_max_distance = 0.0 +lod_max_hysteresis = 0.0 +mesh = SubResource( 3 ) +skeleton = NodePath("..") +material/0 = null + diff --git a/project/character_controller.gd b/project/character_controller.gd index c47511b..72dc2a3 100644 --- a/project/character_controller.gd +++ b/project/character_controller.gd @@ -1,5 +1,7 @@ extends KinematicBody +const BoxPhysics = preload("res://box_physics.gd") + export var speed = 5.0 export var gravity = 9.8 export var jump_force = 5.0 @@ -43,13 +45,19 @@ func _fixed_process(delta): _velocity.z = motor.z _velocity.y -= gravity * delta - if _grounded and Input.is_key_pressed(KEY_SPACE): + #if _grounded and Input.is_key_pressed(KEY_SPACE): + if Input.is_key_pressed(KEY_SPACE): _velocity.y = jump_force - _grounded = false + #_grounded = false var motion = _velocity * delta - var rem = move(motion) + motion = move_with_box_physics(motion) + + assert(delta > 0) + _velocity = motion / delta + + #var rem = move(motion) # TODO Fix it, obsolete code # if is_colliding(): @@ -64,3 +72,60 @@ func _fixed_process(delta): # else: # _grounded = false #get_node("debug").set_text("Grounded=" + str(_grounded)) + + +# TODO There is room for optimization, but I'll leave it like this for now, it doesn't cause any lag +func move_with_box_physics(motion): + var debug3d = get_node("../Debug3D") + + var pos = get_translation() + var box = BoxPhysics.box_from_center_extents(pos, Vector3(0.4, 0.9, 0.4)) + + var expanded_box = BoxPhysics.expand_with_vector(box, motion) + debug3d.draw_wire_box(expanded_box, Color(0,1,0,1)) + + var potential_boxes = [] + + # Collect collisions with the terrain + if has_node(terrain): + var voxel_terrain = get_node(terrain) + var voxels = voxel_terrain.get_storage() + + var min_x = int(floor(expanded_box.position.x)) + var min_y = int(floor(expanded_box.position.y)) + var min_z = int(floor(expanded_box.position.z)) + + var max_x = int(ceil(expanded_box.end.x)) + var max_y = int(ceil(expanded_box.end.y)) + var max_z = int(ceil(expanded_box.end.z)) + + var x = min_x + var y = min_y + var z = min_z + + while z < max_z: + while y < max_y: + while x < max_x: + + var voxel_type = voxels.get_voxel(x,y,z, 0) + if voxel_type != 0: + var voxel_box = Rect3(Vector3(x,y,z), Vector3(1,1,1)) + potential_boxes.append(voxel_box) + debug3d.draw_wire_box(voxel_box) + + x += 1 + x = min_x + y += 1 + y = min_y + z += 1 + + motion = BoxPhysics.get_motion(box, motion, potential_boxes) + move(motion) + return motion + + + + + + + diff --git a/project/debug3d.gd b/project/debug3d.gd new file mode 100644 index 0000000..fe28d2a --- /dev/null +++ b/project/debug3d.gd @@ -0,0 +1,109 @@ +extends MeshInstance + + +var _boxes = [] +var _colors = [] +var _mesh = null + +# Who said "use ImmediateGeometry" node? + +func draw_wire_box(box, color=Color(1,1,1,1)): + _boxes.append(box) + _colors.append(color) + + +func _fixed_process(delta): + + if _mesh == null: + _mesh = ArrayMesh.new() + mesh = _mesh + + if _mesh.get_surface_count() != 0: + _mesh.surface_remove(0) + + var positions = PoolVector3Array() + var colors = PoolColorArray() + var indices = PoolIntArray() + + for i in range(0, _boxes.size()): + var box = _boxes[i] + var color = _colors[i] + + var vi = positions.size() + + var pos = box.position + var end = box.end + + var x0 = pos.x + var y0 = pos.y + var z0 = pos.z + + var x1 = end.x + var y1 = end.y + var z1 = end.z + + positions.append_array([ + Vector3(x0, y0, z0), + Vector3(x1, y0, z0), + Vector3(x1, y0, z1), + Vector3(x0, y0, z1), + + Vector3(x0, y1, z0), + Vector3(x1, y1, z0), + Vector3(x1, y1, z1), + Vector3(x0, y1, z1) + ]) + + colors.append_array([ + color, + color, + color, + color, + color, + color, + color, + color + ]) + + indices.append_array([ + indices.append(vi), + indices.append(vi+1), + indices.append(vi+1), + indices.append(vi+2), + indices.append(vi+2), + indices.append(vi+3), + indices.append(vi+3), + indices.append(vi), + + indices.append(vi+4), + indices.append(vi+5), + indices.append(vi+5), + indices.append(vi+6), + indices.append(vi+6), + indices.append(vi+7), + indices.append(vi+7), + indices.append(vi+4), + + indices.append(vi), + indices.append(vi+4), + indices.append(vi+1), + indices.append(vi+5), + indices.append(vi+2), + indices.append(vi+6), + indices.append(vi+3), + indices.append(vi+7) + ]) + + if positions.size() != 0: + var arrays = [] + # TODO Use ArrayMesh.ARRAY_MAX + arrays.resize(9) + arrays[ArrayMesh.ARRAY_VERTEX] = positions + arrays[ArrayMesh.ARRAY_COLOR] = colors + arrays[ArrayMesh.ARRAY_INDEX] = indices + + _mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays) + + _boxes.clear() + _colors.clear() + diff --git a/project/main.tscn b/project/main.tscn index 24abf24..57539b0 100644 --- a/project/main.tscn +++ b/project/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=2] +[gd_scene load_steps=18 format=2] [ext_resource path="res://terrain_material.tres" type="Material" id=1] [ext_resource path="res://terrain_marerial_transparent.tres" type="Material" id=2] @@ -7,6 +7,7 @@ [ext_resource path="res://character_avatar.tscn" type="PackedScene" id=5] [ext_resource path="res://axes.tscn" type="PackedScene" id=6] [ext_resource path="res://profiling_gui.gd" type="Script" id=7] +[ext_resource path="res://debug3d.gd" type="Script" id=8] [sub_resource type="ProceduralSky" id=1] @@ -195,6 +196,52 @@ subdivide_width = 0 subdivide_height = 0 subdivide_depth = 0 +[sub_resource type="SpatialMaterial" id=9] + +flags_transparent = false +flags_unshaded = true +flags_vertex_lighting = false +flags_on_top = false +flags_use_point_size = false +flags_fixed_size = false +vertex_color_use_as_albedo = true +vertex_color_is_srgb = false +params_diffuse_mode = 0 +params_specular_mode = 0 +params_blend_mode = 0 +params_cull_mode = 0 +params_depth_draw_mode = 0 +params_line_width = 1.0 +params_point_size = 1.0 +params_billboard_mode = 0 +params_grow = false +params_use_alpha_scissor = false +albedo_color = Color( 1, 1, 1, 1 ) +metallic = 0.0 +metallic_specular = 0.5 +metallic_texture_channel = 0 +roughness = 0.0 +roughness_texture_channel = 0 +emission_enabled = false +normal_enabled = false +rim_enabled = false +clearcoat_enabled = false +anisotropy_enabled = false +ao_enabled = false +depth_enabled = false +subsurf_scatter_enabled = false +refraction_enabled = false +detail_enabled = false +uv1_scale = Vector3( 1, 1, 1 ) +uv1_offset = Vector3( 0, 0, 0 ) +uv1_triplanar = false +uv1_triplanar_sharpness = 1.0 +uv2_scale = Vector3( 1, 1, 1 ) +uv2_offset = Vector3( 0, 0, 0 ) +uv2_triplanar = false +uv2_triplanar_sharpness = 1.0 +_sections_unfolded = [ "Emission", "Flags", "Vertex Color" ] + [node name="Node" type="Node"] [node name="WorldEnvironment" type="WorldEnvironment" parent="."] @@ -205,7 +252,7 @@ environment = SubResource( 2 ) provider = SubResource( 3 ) voxel_library = SubResource( 6 ) -view_distance = 64 +view_distance = 32 viewer_path = NodePath("") generate_collisions = true material/0 = ExtResource( 1 ) @@ -221,11 +268,11 @@ _sections_unfolded = [ "material" ] [node name="Grid" type="MeshInstance" parent="."] +visible = false layers = 1 material_override = SubResource( 7 ) cast_shadow = 1 extra_cull_margin = 0.0 -visible_in_all_rooms = false use_in_baked_light = false lod_min_distance = 0.0 lod_min_hysteresis = 0.0 @@ -251,6 +298,7 @@ shadow_color = Color( 0, 0, 0, 1 ) shadow_bias = 0.1 shadow_contact = 0.0 shadow_max_distance = 100.0 +shadow_reverse_cull_face = false editor_only = false directional_shadow_mode = 2 directional_shadow_split_1 = 0.1 @@ -262,17 +310,17 @@ _sections_unfolded = [ "Directional Shadow", "Light", "Shadow" ] [node name="CharacterAvatar" parent="." instance=ExtResource( 5 )] -transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -75.9996, 22.9488, 0 ) -collision/safe_margin = 0.001 -speed = 4.0 -gravity = 0.0 -jump_force = 9.0 +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 25, 17 ) terrain = NodePath("../VoxelTerrain") [node name="axes" parent="." instance=ExtResource( 6 )] [node name="ProfilingInfo" type="Label" parent="."] +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 margin_top = 20.0 margin_right = 104.0 margin_bottom = 34.0 @@ -293,7 +341,6 @@ layers = 1 material_override = null cast_shadow = 1 extra_cull_margin = 0.0 -visible_in_all_rooms = false use_in_baked_light = false lod_min_distance = 0.0 lod_min_hysteresis = 0.0 @@ -304,4 +351,20 @@ skeleton = NodePath("..") material/0 = ExtResource( 1 ) _sections_unfolded = [ "material" ] +[node name="Debug3D" type="MeshInstance" parent="."] + +layers = 1 +material_override = SubResource( 9 ) +cast_shadow = 1 +extra_cull_margin = 0.0 +use_in_baked_light = false +lod_min_distance = 0.0 +lod_min_hysteresis = 0.0 +lod_max_distance = 0.0 +lod_max_hysteresis = 0.0 +mesh = null +skeleton = NodePath("..") +script = ExtResource( 8 ) +_sections_unfolded = [ "Geometry" ] + diff --git a/project/mouse_look.gd b/project/mouse_look.gd index ed69faf..de08b34 100644 --- a/project/mouse_look.gd +++ b/project/mouse_look.gd @@ -5,12 +5,15 @@ export var sensitivity = 0.4 export var min_angle = -90 export var max_angle = 90 export var capture_mouse = true +export var distance = 5.0 var _yaw = 0 var _pitch = 0 +var _offset = Vector3() func _ready(): + _offset = get_translation() if capture_mouse: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) @@ -21,6 +24,14 @@ func _input(event): if capture_mouse: # Capture the mouse Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + + if event.button_index == BUTTON_WHEEL_UP: + distance = max(distance-1, 0) + update_rotations() + + elif event.button_index == BUTTON_WHEEL_DOWN: + distance = max(distance+1, 0) + update_rotations() elif event is InputEventMouseMotion: if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED || not capture_mouse: @@ -39,8 +50,7 @@ func _input(event): _pitch = min_angle+e # Apply rotations - set_rotation(Vector3(0, deg2rad(_yaw), 0)) - rotate(get_transform().basis.x.normalized(), -deg2rad(_pitch)) + update_rotations() elif event is InputEventKey: if event.pressed: @@ -54,5 +64,10 @@ func _input(event): print("Position: ", pos, ", Forward: ", fw) +func update_rotations(): + set_translation(Vector3()) + set_rotation(Vector3(0, deg2rad(_yaw), 0)) + rotate(get_transform().basis.x.normalized(), -deg2rad(_pitch)) + set_translation(get_transform().basis.z * distance + _offset) diff --git a/project/project.godot b/project/project.godot index e4752d0..368d918 100644 --- a/project/project.godot +++ b/project/project.godot @@ -1,14 +1,26 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + config_version=3 + [application] +run/main_scene="res://main.tscn" name="Voxel Game" main_scene="res://main.tscn" icon="res://icon.png" [input] -action1=[ InputEvent(MBUTTON,1) ] -action2=[ InputEvent(MBUTTON,2) ] +action1=[ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null) + ] +action2=[ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null) + ] [layer_names]