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.

167 lines
5.5 KiB

2 months ago
public partial class Terrain
: StaticBody3D
{
[Export] public Vector2I Size { get; set; } = new(64, 64);
[Export] public float TileSize { get; set; } = 2.0f;
[Export] public ShaderMaterial Material { get; set; }
2 months ago
// 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; }
2 months ago
Material _editToolMaterial;
public override void _Ready()
{
_editToolMaterial = new StandardMaterial3D {
VertexColorUseAsAlbedo = true,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true,
};
UpdateMeshAndShape();
}
public Tile GetTile(TilePos pos)
=> (Tiles?.TryGetValue(pos.ToVector2I(), out var result) == true)
? Tile.FromDictionary(result.AsGodotDictionary()) : default;
public void SetTile(TilePos pos, Tile value)
{
var key = pos.ToVector2I();
var dict = value.ToDictionary();
if (dict == null) Tiles.Remove(key);
else Tiles[key] = dict;
}
public bool Contains(TilePos pos)
=> (pos.X >= 0) && (pos.X < Size.X)
&& (pos.Y >= 0) && (pos.Y < Size.Y);
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary>
public TilePos ToTilePos(Vector3 localPos)
2 months ago
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f),
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f));
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);
}
// TODO: Don't hardcode.
var num_textures = 4;
var num_blend_textures = 7;
var rnd = new Random();
for (var x = 0; x < Size.X; x++)
2 months ago
for (var z = 0; z < Size.Y; z++) {
var tile = GetTile(new(x, z));
var corners = GetTileCornerPositions(new(x, z), tile);
// Randomly select two different textures and one blend texture.
mesh.SurfaceSetColor(new(
(float)tile.TexturePrimary / num_textures,
(float)tile.TextureSecondary / num_textures,
(float)tile.TextureBlend / num_blend_textures
));
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.
// This is done by finding the corner with the least variance between its neighboring corners.
var minDiff = Abs(sorted[0].Height - sorted[2].Height); // Difference between lowest and 3rd lowest point.
var maxDiff = Abs(sorted[3].Height - sorted[1].Height); // Difference between highest and 3rd highest point.
var first = sorted[(minDiff > maxDiff) ? 0 : 3].Corner;
if (first is Corner.TopLeft or Corner.BottomRight) {
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));
} else {
AddTriangle(corners.TopRight , new(1.0f, 0.0f),
corners.BottomRight, new(1.0f, 1.0f),
corners.TopLeft , new(0.0f, 0.0f));
AddTriangle(corners.BottomRight, new(1.0f, 1.0f),
corners.BottomLeft , new(0.0f, 1.0f),
corners.TopLeft , new(0.0f, 0.0f));
}
2 months ago
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, Material);
2 months ago
shape.Data = [.. points];
}
public Corners<Vector3> GetTileCornerPositions(TilePos pos)
=> GetTileCornerPositions(pos, GetTile(pos));
public Corners<Vector3> GetTileCornerPositions(TilePos pos, Tile tile)
{
var half = TileSize / 2;
var vx = (pos.X - Size.X / 2.0f) * TileSize;
var vz = (pos.Y - Size.Y / 2.0f) * TileSize;
return new(new(vx - half, tile.Height.TopLeft , vz - half),
new(vx + half, tile.Height.TopRight , vz - half),
new(vx + half, tile.Height.BottomRight, vz + half),
new(vx - half, tile.Height.BottomLeft , vz + half));
}
2 months ago
ImmediateMesh GetOrCreateMesh(string name)
{
var meshInstance = (MeshInstance3D)GetNodeOrNull(name);
if (meshInstance == null) {
meshInstance = new(){ Name = name, Mesh = new ImmediateMesh() };
2 months ago
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() };
2 months ago
AddChild(collisionShape);
collisionShape.Owner = this;
}
return (ConcavePolygonShape3D)collisionShape.Shape;
}
}