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.
 
 

282 lines
9.4 KiB

public partial class TerrainEditingControls
{
bool _unhandledMotion = false; // Used to detect when mouse moves off the terrain.
TilePos? _tileHover = null; // Position of currently hovered tile.
bool _isSelecting = false; // Whether left mouse is held down to select tiles.
(TilePos, TilePos)? _selection = null;
Material _editToolMaterial;
void OnEditingReady()
{
_editToolMaterial = new StandardMaterial3D {
VertexColorUseAsAlbedo = true,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true,
};
}
public override void _Input(InputEvent ev)
{
if (GetTerrain() is not Terrain terrain) return;
if (Engine.IsEditorHint()) {
// Make sure to transform the input event to the 3D scene's viewport.
var viewport = EditorInterface.Singleton.GetEditorViewport3D();
var container = viewport.GetParent<SubViewportContainer>();
ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse());
if (ev is InputEventMouse m) m.GlobalPosition = m.Position;
}
if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) {
_isSelecting = false;
GetViewport().SetInputAsHandled();
return;
}
if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true })
&& (wheel is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
{
// NOTE: Potential bug in the Godot editor?
// Does it zoom both when mouse wheel is "pressed" and "released"?
// Because just cancelling one of them still causes zooming to occur.
GetViewport().SetInputAsHandled();
if (!pressed) return;
const float AdjustHeight = 0.5f;
var amount = (wheel == MouseButton.WheelUp)
? AdjustHeight : -AdjustHeight;
var selection = TileRegion.From(_selection.Value);
// Raise connected corners.
foreach (var innerCorner in Enum.GetValues<Corner>()) {
var outerCorner = innerCorner.GetOpposite();
var innerPos = selection.GetTileFor(innerCorner);
var outerPos = innerPos.GetNeighbor(innerCorner);
var outerTile = terrain.GetTile(outerPos);
var innerHeight = terrain.GetTile(innerPos).Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
terrain.SetTile(outerPos, outerTile);
}
}
// Raise connected sides.
foreach (var side in Enum.GetValues<Side>()) {
foreach (var innerPos in selection.GetTilesFor(side)) {
var outerPos = innerPos.GetNeighbor(side);
var innerTile = terrain.GetTile(innerPos);
var outerTile = terrain.GetTile(outerPos);
var (innerCorner1, innerCorner2) = side.GetCorners();
var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
var changed = false;
var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) };
foreach (var (innerCorner, outerCorner) in matchingCorners) {
var innerHeight = innerTile.Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
changed = true;
}
}
if (changed) terrain.SetTile(outerPos, outerTile);
}
}
// Raise selected tiles themselves.
foreach (var pos in selection.GetAllTiles()) {
var tile = terrain.GetTile(pos);
tile.Height.Adjust(amount);
terrain.SetTile(pos, tile);
}
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
return;
}
if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true })
&& (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
{
GetViewport().SetInputAsHandled();
if (!pressed2) return;
var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1;
var selection = TileRegion.From(_selection.Value);
foreach (var pos in selection.GetAllTiles()) {
var tile = terrain.GetTile(pos);
tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4);
terrain.SetTile(pos, tile);
}
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
return;
}
if (ev is InputEventMouseMotion)
_unhandledMotion = true;
if ((ev is InputEventMouse mouse) && (!Engine.IsEditorHint() ||
EditorInterface.Singleton.GetEditorViewport3D()
.GetVisibleRect().HasPoint(mouse.Position)))
OnInputRayCastTerrain(terrain, mouse);
}
void OnInputRayCastTerrain(Terrain terrain, 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");
if (collider == terrain) {
var pos = (Vector3)result["position"];
// var normal = (Vector3)result["normal"];
// var shape = (int)result["shape"];
OnTerrainInput(terrain, ev, pos);
}
}
void OnTerrainInput(Terrain terrain, InputEvent ev, Vector3 position)
{
if (terrain == null) return;
var localPos = terrain.ToLocal(position);
var tilePos = terrain.ToTilePos(localPos);
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
_selection = new(tilePos, tilePos);
_isSelecting = true;
GetViewport().SetInputAsHandled();
return;
}
if (ev is InputEventMouseMotion) {
_unhandledMotion = false;
_tileHover = tilePos;
if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos };
}
}
public override void _Process(double delta)
{
if (GetTerrain() is not Terrain terrain) {
_tileHover = null;
_selection = null;
_isSelecting = false;
terrain = null;
}
if (_unhandledMotion)
_tileHover = null;
if ((_tileHover != null) || (_selection != null)) {
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 bottomLeft, Vector3 bottomRight) {
AddLine(topLeft , topRight );
AddLine(topRight , bottomRight);
AddLine(bottomRight, bottomLeft );
AddLine(bottomLeft , topLeft );
}
if (_tileHover is TilePos hover) {
var corners = terrain.GetTileCornerPositions(hover);
var margin = 0.1f;
mesh.SurfaceSetColor(Colors.Black);
AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin),
corners.TopRight + new Vector3(+margin, 0, -margin),
corners.BottomLeft + new Vector3(-margin, 0, +margin),
corners.BottomRight + new Vector3(+margin, 0, +margin));
}
mesh.SurfaceSetColor(Colors.Blue);
if (_selection is (TilePos, TilePos) selection)
foreach (var pos in TileRegion.From(selection).GetAllTiles()) {
var corners = terrain.GetTileCornerPositions(pos);
AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight);
}
mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, _editToolMaterial);
} else {
var meshInstance = (MeshInstance3D)GetNodeOrNull("EditToolMesh");
var mesh = (ImmediateMesh)meshInstance?.Mesh;
mesh?.ClearSurfaces();
}
}
public Terrain GetTerrain() => EditorInterface.Singleton.GetSelection()
.GetSelectedNodes().OfType<Terrain>().FirstOrDefault();
readonly record struct TileRegion(int Left, int Top, int Right, int Bottom)
{
public TilePos TopLeft => new(Left , Top);
public TilePos TopRight => new(Right, Top);
public TilePos BottomRight => new(Right, Bottom);
public TilePos BottomLeft => new(Left , Bottom);
public int Width => Right - Left + 1;
public int Height => Bottom - Top + 1;
public static TileRegion From((TilePos, TilePos) selection)
=> From(selection.Item1, selection.Item2);
public static TileRegion From(TilePos a, TilePos b)
=> new(Min(a.X, b.X), Min(a.Y, b.Y), Max(a.X, b.X), Max(a.Y, b.Y));
public TilePos GetTileFor(Corner corner)
=> corner switch {
Corner.TopLeft => TopLeft,
Corner.TopRight => TopRight,
Corner.BottomRight => BottomRight,
Corner.BottomLeft => BottomLeft,
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
};
public IEnumerable<TilePos> GetTilesFor(Side side)
{
var (left, top, right, bottom) = this;
return side switch {
Side.Left => Enumerable.Range(Top, Height).Select(y => new TilePos(left, y)),
Side.Top => Enumerable.Range(Left, Width).Select(x => new TilePos(x, top)),
Side.Right => Enumerable.Range(Top, Height).Select(y => new TilePos(right, y)),
Side.Bottom => Enumerable.Range(Left, Width).Select(x => new TilePos(x, bottom)),
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)),
};
}
public IEnumerable<TilePos> GetAllTiles()
{
for (var x = Left; x <= Right; x++)
for (var y = Top; y <= Bottom; y++)
yield return new(x, y);
}
}
}