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.

218 lines
7.7 KiB

public partial class TerrainEditingControls
{
Terrain _currentTerrain = null;
Material _editToolMaterial;
public override void _EnterTree()
{
_editToolMaterial = new StandardMaterial3D {
AlbedoColor = Colors.Blue,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true,
};
}
public override void _ExitTree()
=> ClearEditToolMesh();
public override void _Input(InputEvent ev)
{
var viewport = !Engine.IsEditorHint() ? GetViewport()
: EditorInterface.Singleton.GetEditorViewport3D();
if (Engine.IsEditorHint()) {
// Make sure to transform the input event to the 3D scene's viewport.
var container = viewport.GetParent<SubViewportContainer>();
ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse());
if (ev is InputEventMouse m) m.GlobalPosition = m.Position;
}
if (ev is InputEventMouse mouse) {
if (viewport.GetVisibleRect().HasPoint(mouse.Position)
&& (RayCastTerrain(mouse) is var (terrain, position))) {
var (tile, corner) = FindClosestTile(terrain, position);
var drawSize = (ToolShape == ToolShape.Corner) ? 1 : DrawSize;
var isEven = (drawSize % 2) == 0;
var radius = FloorToInt(drawSize / 2.0f);
// Offset tile position by corner.
if (isEven) tile = corner switch {
Corner.TopLeft => new(tile.X + 0, tile.Y + 0),
Corner.TopRight => new(tile.X + 1, tile.Y + 0),
Corner.BottomRight => new(tile.X + 1, tile.Y + 1),
Corner.BottomLeft => new(tile.X + 0, tile.Y + 1),
_ => throw new InvalidOperationException(),
};
IEnumerable<TilePos> GetTilesInSquare() {
var minX = tile.X - radius;
var minY = tile.Y - radius;
var maxX = tile.X + radius - (isEven ? 1 : 0);
var maxY = tile.Y + radius - (isEven ? 1 : 0);
for (var x = minX; x <= maxX; x++)
for (var y = minY; y <= maxY; y++)
yield return new(x, y);
}
IEnumerable<TilePos> GetTilesInRadius() {
var center = isEven
? new Vector2(tile.X , tile.Y )
: new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
var distanceSqr = Pow(isEven ? radius - 0.25f : radius + 0.25f, 2);
return GetTilesInSquare()
.Where(tile => center.DistanceSquaredTo(
new Vector2(tile.X + 0.5f, tile.Y + 0.5f)) < distanceSqr);
}
var tiles = (ToolShape switch {
// TODO: Edit corner, not full tile.
ToolShape.Corner => [tile],
ToolShape.Circle => GetTilesInRadius(),
ToolShape.Square => GetTilesInSquare(),
_ => throw new InvalidOperationException(),
}).ToHashSet();
// TODO: Handle different tool modes, such as painting.
// TODO: Add a flatten tool mode: Flattens everything selected to nearest corner height.
// TODO: Allow click-dragging which doesn't affect already changed tiles / corners.
// TODO: Support undo and redo.
// TODO: Support "disconnected" mode which can create vertical cliffs.
// Raise / lower the terrain if left / right mouse button is pressed.
if ((mouse is InputEventMouseButton { ButtonIndex: var button, Pressed: true })
&& (button is MouseButton.Left or MouseButton.Right))
{
GetViewport().SetInputAsHandled();
const float AdjustHeight = 0.5f;
var amount = (button == MouseButton.Left)
? AdjustHeight : -AdjustHeight;
// Find corners that are "connected" and should be raised.
var corners = new HashSet<(TilePos Position, Corner Corner)>();
foreach (var pos in tiles) {
var tile2 = terrain.GetTile(pos);
foreach (var corner2 in Enum.GetValues<Corner>()) {
var height = tile2.Height[corner2];
foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner2)) {
if (tiles.Contains(neighborPos)) continue;
var neighborHeight = terrain.GetTile(neighborPos).Height[neighborCorner];
if (neighborHeight == height) corners.Add((neighborPos, neighborCorner));
}
}
}
// Raise connected corners.
foreach (var group in corners.GroupBy(e => e.Position, e => e.Corner)) {
var pos = group.Key;
var tile2 = terrain.GetTile(pos);
foreach (var corner2 in group)
tile2.Height[corner2] += amount;
terrain.SetTile(pos, tile2);
}
// Raise selected tiles themselves.
foreach (var pos in tiles) {
var tile2 = terrain.GetTile(pos);
tile2.Height.Adjust(amount);
terrain.SetTile(pos, tile2);
}
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
}
UpdateEditToolMesh(terrain, tiles);
} else {
ClearEditToolMesh();
}
}
}
void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> tiles)
{
if (terrain != _currentTerrain) ClearEditToolMesh();
_currentTerrain = terrain;
var mesh = terrain.GetOrCreateMesh("EditToolMesh");
mesh.ClearSurfaces();
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines);
void AddLine(Vector3 start, Vector3 end) {
mesh.SurfaceAddVertex(start);
mesh.SurfaceAddVertex(end);
}
void AddQuad(Vector3 topLeft , Vector3 topRight,
Vector3 bottomRight, Vector3 bottomLeft) {
AddLine(topLeft , topRight );
AddLine(topRight , bottomRight);
AddLine(bottomRight, bottomLeft );
AddLine(bottomLeft , topLeft );
}
foreach (var tile in tiles) {
var (topLeft, topRight, bottomRight, bottomLeft)
= terrain.GetTileCornerPositions(tile);
AddQuad(topLeft, topRight, bottomRight, bottomLeft);
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, _editToolMaterial);
}
void ClearEditToolMesh()
=> _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree();
(Terrain Terrain, Vector3 Position)? RayCastTerrain(InputEventMouse ev)
{
// Ray is cast from the editor camera's view.
var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D();
var from = camera.ProjectRayOrigin(ev.Position);
var to = from + camera.ProjectRayNormal(ev.Position) * camera.Far;
// Actual collision is done in the edited scene though.
var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot();
var space = root.GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(from, to);
var result = space.IntersectRay(query);
var collider = (GodotObject)result.GetValueOrDefault("collider");
return (collider is Terrain terrain)
? (terrain, (Vector3)result["position"])
: null;
}
static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position)
{
var local = terrain.ToLocal(position);
var tileX = local.X / terrain.TileSize + 0.5 + terrain.Size.X / 2;
var tileY = local.Z / terrain.TileSize + 0.5 + terrain.Size.Y / 2;
var tile = new TilePos(FloorToInt(tileX), FloorToInt(tileY));
var cornerX = RoundToInt(PosMod(tileX, 1));
var cornerY = RoundToInt(PosMod(tileY, 1));
var corner = (cornerX, cornerY) switch {
(0, 0) => Corner.TopLeft,
(1, 0) => Corner.TopRight,
(1, 1) => Corner.BottomRight,
(0, 1) => Corner.BottomLeft,
_ => throw new InvalidOperationException(),
};
return (tile, corner);
}
static readonly Dictionary<Corner, (int X, int Y, Corner Opposite)[]> _offsetLookup = new(){
[Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )],
[Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)],
[Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )],
[Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )],
};
static IEnumerable<(TilePos, Corner)> GetNeighbors(TilePos pos, Corner corner)
=> _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite));
}