A game where you get to play as a slime, made with Godot.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

204 lines
6.4 KiB

public partial class Terrain
: StaticBody3D
{
[Export] public Vector2I Size { get; set; } = new(64, 64);
[Export] public float TileSize { get; set; } = 2.0f;
[Export] public Godot.Collections.Array<Texture2D> Textures { get; set; }
// If value at position non-existant => [ 0, 0, 0, 0 ]
// If value at position is float => [ v, v, v, v ]
// If value at position is float[] => value
[Export] public Godot.Collections.Dictionary<Vector2I, Variant> Tiles { get; set; }
Material _editToolMaterial;
Material _terrainMaterial;
public override void _Ready()
{
_editToolMaterial = new StandardMaterial3D {
VertexColorUseAsAlbedo = true,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true,
};
_terrainMaterial = new StandardMaterial3D {
AlbedoTexture = Textures?.FirstOrDefault(),
};
UpdateMeshAndShape();
}
/// <summary> Returns if this terrain grid contains the specified tile position. </summary>
public bool ContainsTilePos(Vector2I tilePos)
=> (tilePos.X >= 0) && (tilePos.X < Size.X)
&& (tilePos.Y >= 0) && (tilePos.Y < Size.Y);
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary>
public Vector2I ToTilePos(Vector3 localPos)
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f),
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f));
public float GetCornerHeight(Vector2I tilePos, Corner corner)
=> GetTileRaw(tilePos) switch {
{ VariantType: Variant.Type.Nil } => 0,
{ VariantType: Variant.Type.Float } num => (float)num,
{ VariantType: Variant.Type.PackedFloat32Array } nums => ((float[])nums)[(int)corner],
_ => throw new Exception("Invalid type in Points dictionary"),
};
public void SetCornerHeight(Vector2I tilePos, Corner corner, float value)
{
if (!ContainsTilePos(tilePos)) return;
var existing = GetTileRaw(tilePos) switch {
{ VariantType: Variant.Type.Nil } => [ 0, 0, 0, 0 ],
{ VariantType: Variant.Type.Float } num => [ (float)num, (float)num, (float)num, (float)num ],
{ VariantType: Variant.Type.PackedFloat32Array } nums => (float[])nums,
_ => throw new Exception("Invalid type in Points dictionary"),
};
existing[(int)corner] = value;
if (existing.All(IsZeroApprox))
Tiles?.Remove(tilePos);
if (IsEqualApprox(existing[0], existing[1])
&& IsEqualApprox(existing[0], existing[2])
&& IsEqualApprox(existing[0], existing[3]))
SetTileRaw(tilePos, existing[0]);
else
SetTileRaw(tilePos, existing);
}
public void SetTileHeight(Vector2I tilePos, float value)
{
if (!ContainsTilePos(tilePos)) return;
if (IsZeroApprox(value)) Tiles?.Remove(tilePos);
else SetTileRaw(tilePos, value);
}
public void AdjustTileHeight(Vector2I tilePos, float relative)
{
if (relative == 0) return;
if (!ContainsTilePos(tilePos)) return;
switch (GetTileRaw(tilePos)) {
case { VariantType: Variant.Type.Nil }:
SetTileRaw(tilePos, relative);
break;
case { VariantType: Variant.Type.Float } num:
var newNum = (float)num + relative;
if (IsZeroApprox(newNum)) Tiles?.Remove(tilePos);
else SetTileRaw(tilePos, newNum);
break;
case { VariantType: Variant.Type.PackedFloat32Array } nums:
var newNums = (float[])nums;
for (var i = 0; i < 4; i++) newNums[i] += relative;
SetTileRaw(tilePos, newNums);
break;
default: throw new Exception("Invalid type in Points dictionary");
};
}
public GridCorners GetGridCorners(Vector2I tilePos)
{
var halfSize = TileSize / 2;
var vx = (tilePos.X - Size.X / 2.0f) * TileSize;
var vz = (tilePos.Y - Size.Y / 2.0f) * TileSize;
return new() {
TopLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.TopLeft ), vz - halfSize),
TopRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.TopRight ), vz - halfSize),
BottomLeft = new(vx - halfSize, GetCornerHeight(tilePos, Corner.BottomLeft ), vz + halfSize),
BottomRight = new(vx + halfSize, GetCornerHeight(tilePos, Corner.BottomRight), vz + halfSize),
};
}
Variant GetTileRaw(Vector2I tilePos)
=> (Tiles?.TryGetValue(tilePos, out var result) == true) ? result : default;
void SetTileRaw(Vector2I tilePos, Variant value)
=> (Tiles ??= [])[tilePos] = value;
void UpdateMeshAndShape()
{
var mesh = GetOrCreateMesh("MeshInstance");
var shape = GetOrCreateShape("CollisionShape");
mesh.ClearSurfaces();
mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles);
var points = new List<Vector3>();
void AddPoint(Vector3 pos, Vector2 uv) {
mesh.SurfaceSetUV(uv);
mesh.SurfaceAddVertex(pos);
points.Add(pos);
}
void AddTriangle(Vector3 v1, Vector2 uv1,
Vector3 v2, Vector2 uv2,
Vector3 v3, Vector2 uv3) {
var dir = (v3 - v1).Cross(v2 - v1);
mesh.SurfaceSetNormal(dir.Normalized());
AddPoint(v1, uv1);
AddPoint(v2, uv2);
AddPoint(v3, uv3);
}
for (var x = 0; x < Size.X; x++) {
for (var z = 0; z < Size.Y; z++) {
var corners = GetGridCorners(new(x, z));
AddTriangle(corners.TopLeft , new(0.0f, 0.0f),
corners.TopRight , new(1.0f, 0.0f),
corners.BottomLeft , new(0.0f, 1.0f));
AddTriangle(corners.TopRight , new(1.0f, 0.0f),
corners.BottomRight, new(1.0f, 1.0f),
corners.BottomLeft , new(0.0f, 1.0f));
}
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, _terrainMaterial);
shape.Data = [.. points];
}
ImmediateMesh GetOrCreateMesh(string name)
{
var meshInstance = (MeshInstance3D)GetNodeOrNull(name);
if (meshInstance == null) {
meshInstance = new() { Name = name, Mesh = new ImmediateMesh() };
AddChild(meshInstance);
meshInstance.Owner = this;
}
return (ImmediateMesh)meshInstance.Mesh;
}
ConcavePolygonShape3D GetOrCreateShape(string name)
{
var collisionShape = (CollisionShape3D)GetNodeOrNull(name);
if (collisionShape == null) {
collisionShape = new() { Name = name, Shape = new ConcavePolygonShape3D() };
AddChild(collisionShape);
collisionShape.Owner = this;
}
return (ConcavePolygonShape3D)collisionShape.Shape;
}
public enum Corner
{
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
public readonly struct GridCorners
{
public Vector3 TopLeft { get; init; }
public Vector3 TopRight { get; init; }
public Vector3 BottomLeft { get; init; }
public Vector3 BottomRight { get; init; }
}
}