Compare commits

...

4 Commits

  1. 68
      player/CameraController.cs
  2. 21
      player/character.tscn
  3. 1
      terrain/Terrain+Editing.cs
  4. 15
      terrain/Terrain.cs
  5. 6
      terrain/TerrainChunk.cs
  6. 3
      terrain/Tile.cs
  7. 116
      terrain/terrain_material.tres
  8. 20
      utility/GodotExtensions.cs
  9. 12
      utility/MathExtensions.cs

@ -1,17 +1,31 @@
public partial class CameraController : SpringArm3D public partial class CameraController : Camera3D
{ {
[ExportCategory("Follow")]
[Export] public float FollowDistance { get; set; } = 1.2f;
[Export] public float FollowYOffset { get; set; } = 1.0f;
[Export] public float FollowSmoothing { get; set; } = 12.0f;
[ExportCategory("Rotation")]
[Export] public Vector2 MouseSensitivity { get; set; } = new(0.2f, 0.2f); // Degrees per pixel. [Export] public Vector2 MouseSensitivity { get; set; } = new(0.2f, 0.2f); // Degrees per pixel.
[Export] public float PitchMinimum { get; set; } = -90; [Export] public float PitchMinimum { get; set; } = 20;
[Export] public float PitchMaximum { get; set; } = -25; [Export] public float PitchMaximum { get; set; } = 65;
[Export] public float PitchSmoothing { get; set; } = 12.0f;
// FIXME: Fix the "snappyness" when moving slowly.
// TODO: Add a "soft" minimum / maximum for pitch.
// TODO: Gradually return to maximum spring length. // TODO: Gradually return to maximum spring length.
// TODO: Turn player transparent as the camera moves closer. // TODO: Turn player transparent as the camera moves closer.
public static bool IsMouseCaptured public static bool IsMouseCaptured
=> Input.MouseMode == Input.MouseModeEnum.Captured; => Input.MouseMode == Input.MouseModeEnum.Captured;
Node3D _player;
Vector3 _smoothPlayerPos;
public override void _Ready()
{
_player = this.GetParentOrThrow<Node3D>();
_smoothPlayerPos = _player.GlobalPosition;
Transform = _player.GlobalTransform.Translated(new(0.0f, FollowYOffset, 0.0f));
}
public override void _Input(InputEvent ev) public override void _Input(InputEvent ev)
{ {
if (IsMouseCaptured && ev.IsActionPressed("ui_cancel")) { if (IsMouseCaptured && ev.IsActionPressed("ui_cancel")) {
@ -37,11 +51,45 @@ public partial class CameraController : SpringArm3D
void ApplyRotation(Vector2 delta) void ApplyRotation(Vector2 delta)
{ {
delta *= Tau / 360; // degrees to radians var (pitch, yaw, _) = RotationDegrees;
var (pitch, yaw, _) = Rotation;
yaw += delta.X;
pitch += delta.Y; pitch += delta.Y;
pitch = Clamp(pitch, DegToRad(PitchMinimum), DegToRad(PitchMaximum)); yaw += delta.X;
Rotation = new(pitch, yaw, 0); RotationDegrees = new(pitch, yaw, 0);
}
public override void _PhysicsProcess(double delta)
{
_smoothPlayerPos = _smoothPlayerPos.Damp(_player.GlobalPosition, FollowSmoothing, delta);
var target = _smoothPlayerPos
+ Basis.Z * FollowDistance
+ Vector3.Up * FollowYOffset;
Position = OffsetRayIntersection(_smoothPlayerPos, target, 0.2f);
}
Vector3 OffsetRayIntersection(Vector3 from, Vector3 to, float margin)
{
const PhysicsLayer CollisionMask = PhysicsLayer.Terrain
| PhysicsLayer.Objects;
var query = PhysicsRayQueryParameters3D.Create(from, to, (uint)CollisionMask);
var result = GetWorld3D().DirectSpaceState.IntersectRay(query);
if (result.Count > 0) {
var hit = (Vector3)result["position"];
var safeDistance = Max(0, from.DistanceTo(hit) - margin);
return from + (to - from).Normalized() * safeDistance;
} else {
// No intersection occured,
return to;
}
}
public override void _Process(double delta)
{
var pitch = RotationDegrees.X;
var (min, max) = (-PitchMaximum, -PitchMinimum);
if (pitch < min) pitch = pitch.Damp(min, PitchSmoothing, delta);
if (pitch > max) pitch = pitch.Damp(max, PitchSmoothing, delta);
RotationDegrees = RotationDegrees with { X = pitch };
} }
} }

@ -20,25 +20,18 @@ collision_mask = 3
floor_max_angle = 0.698132 floor_max_angle = 0.698132
[node name="MeshInstance3D" type="MeshInstance3D" parent="."] [node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0)
mesh = SubResource("SphereMesh_7ljg8") mesh = SubResource("SphereMesh_7ljg8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.2, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
shape = SubResource("SphereShape3D_6qbb2") shape = SubResource("SphereShape3D_6qbb2")
[node name="MovementController" type="Node" parent="."] [node name="Camera" type="Camera3D" parent="."]
script = ExtResource("1_akh08") top_level = true
[node name="CameraArm" type="SpringArm3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.766044, 0.642788, 0, -0.642788, 0.766044, 0, 0.1, 0)
collision_mask = 3
spring_length = 2.0
margin = 0.05
script = ExtResource("2_2din1")
[node name="Camera" type="Camera3D" parent="CameraArm"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
current = true current = true
size = 4.5 size = 4.5
far = 500.0 far = 500.0
script = ExtResource("2_2din1")
[node name="MovementController" type="Node" parent="."]
script = ExtResource("1_akh08")

@ -1,5 +1,4 @@
using System.IO; using System.IO;
using System.Runtime.InteropServices;
public partial class Terrain public partial class Terrain
{ {

@ -62,12 +62,19 @@ public partial class Terrain
SetTexture(tile.TexturePrimary, tile.TextureSecondary, tile.TextureBlend); SetTexture(tile.TexturePrimary, tile.TextureSecondary, tile.TextureBlend);
var sorted = new (Corner Corner, float Height)[] {
(Corner.TopLeft , tile.Height.TopLeft ),
(Corner.TopRight , tile.Height.TopRight ),
(Corner.BottomRight, tile.Height.BottomRight),
(Corner.BottomLeft , tile.Height.BottomLeft ),
};
Array.Sort(sorted, (a, b) => a.Height.CompareTo(b.Height));
// Find the "ideal way" to split the quad for the tile into two triangles. // Find the "ideal way" to split the quad for the tile into two triangles.
// This is done by finding the corner with the least variance between its neighboring corners. // This is done by finding the corner with the least variance between its neighboring corners.
var sorted = tile.Height.ToArray(); Array.Sort(sorted); var minDiff = Abs(sorted[0].Height - sorted[2].Height); // Difference between lowest and 3rd lowest point.
var minDiff = Abs(sorted[0] - sorted[2]); // Difference between lowest and 3rd lowest point. var maxDiff = Abs(sorted[3].Height - sorted[1].Height); // Difference between highest and 3rd highest point.
var maxDiff = Abs(sorted[3] - sorted[1]); // Difference between highest and 3rd highest point. var first = sorted[(minDiff > maxDiff) ? 0 : 3].Corner;
var first = (Corner)sorted[(minDiff > maxDiff) ? 0 : 3];
if (first is Corner.TopLeft or Corner.BottomRight) { if (first is Corner.TopLeft or Corner.BottomRight) {
AddTriangle(corners.TopLeft , new(0.0f, 0.0f), AddTriangle(corners.TopLeft , new(0.0f, 0.0f),

@ -15,6 +15,12 @@ public partial class TerrainChunk
[Export] public byte[] Data { get; set; } = new byte[SizeInBytes]; [Export] public byte[] Data { get; set; } = new byte[SizeInBytes];
public bool IsEmpty { get {
foreach (var b in Data)
if (b != 0) return false;
return true;
} }
public ref Tile this[TilePos pos] { get { public ref Tile this[TilePos pos] { get {
var tiles = MemoryMarshal.Cast<byte, Tile>(Data); var tiles = MemoryMarshal.Cast<byte, Tile>(Data);
return ref tiles[GetIndex(pos)]; return ref tiles[GetIndex(pos)];

@ -61,9 +61,6 @@ public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
} } } }
} }
public readonly T[] ToArray()
=> [ TopLeft, TopRight, BottomRight, BottomLeft ];
public readonly bool Equals(Corners<T> other) public readonly bool Equals(Corners<T> other)
=> TopLeft .Equals(other.TopLeft ) => TopLeft .Equals(other.TopLeft )
&& TopRight .Equals(other.TopRight ) && TopRight .Equals(other.TopRight )

File diff suppressed because one or more lines are too long

@ -1,7 +1,23 @@
public static class GodotExtensions public static class GodotExtensions
{ {
public static Vector2I RoundToVector2I(this Vector2 vector) public static T GetParentOrThrow<T>(this Node node)
=> new(RoundToInt(vector.X), RoundToInt(vector.Y)); where T : class
{
var parent = node.GetParent();
if (parent == null) throw new InvalidOperationException($"Parent of {node} is null");
if (parent is not T result) throw new InvalidCastException($"Parent of {node} is {node.GetType()}, not {typeof(T)}");
return result;
}
public static T GetNodeOrThrow<T>(this Node parent, NodePath path)
where T : class
{
var node = parent.GetNodeOrNull(path);
if (node == null) throw new InvalidOperationException($"Could not find node {path} from {parent}");
if (node is not T result) throw new InvalidCastException($"Node {path} from {parent} is {node.GetType()}, not {typeof(T)}");
return result;
}
public static (Corner, Corner) GetCorners(this Side side) public static (Corner, Corner) GetCorners(this Side side)
=> side switch { => side switch {

@ -0,0 +1,12 @@
public static class MathExtensions
{
// Framerate independent dampening functions, similar to lerp.
// https://rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
public static float Damp(this float from, float to, float lambda, double delta)
=> Lerp(from, to, 1 - Exp(-lambda * (float)delta));
public static Vector3 Damp(this Vector3 from, Vector3 to, float lambda, double delta)
=> from.Lerp(to, 1 - Exp(-lambda * (float)delta));
public static Vector2I RoundToVector2I(this Vector2 vector)
=> new(RoundToInt(vector.X), RoundToInt(vector.Y));
}
Loading…
Cancel
Save