Using RayCast3d with a topdown camera

Godot Version

4.3

I am building a top down, tile based strategy game. The camera is placed in a fixed height looking down at the board and is controlled by a gamepad.
Goal: The player can select the tile that the camera is hovering over.

I’ve simplified the project to the components which are relevant to the problem:

scenebasetile

  • ‘BaseTile’ is of type Area3d
  • Node ‘TileCollision’ of type CollisionShape3D
  • Node ‘HexRim’ of type Node3D. This is an imported mesh

Both with default Transform

scenebasetileed

using Godot;
using System;
public partial class BaseTile : Area3D
{
	public override void _Ready()
	{
		GD.Print("Tile position:" + Position.ToString());
	}
}

mapscene

  • 'Map; is type Node3D
  • Node ‘OverheadCameraBody’ of type ‘CharacterBody3D’, default Transform.
  • Node ‘RayCastingCamera’ of type ‘Camera3D’. Defualt Position. Rotatio (-90,0,0) so it looks down at the board.
  • Node ‘DownwardRay’ of type ‘RayCast3D’. Default Transform.
  • Node ‘DisabledCollision3D’ of type ‘CollisionShape3D’. It’s here to stop Godot from bitching. It’s disabled so shouldn’t interfere with the ray.
  • Node ‘CameraPositionMarker’ of type ‘MeshInstance3D’. Position (0,-2,0). This provides a sort of a cursor to see where the camera is.
  • Node ‘Tile’ Of type ‘BaseTile’. Default transform.
using Godot;
using System;

public partial class RayCastingCamera : Camera3D
{
	private RayCast3D downwardRay;
	private GodotObject detectedCollider;
	
	public override void _Ready()
	{
		downwardRay = GetNode<RayCast3D>("DownwardRay");
		GD.Print("Downward ray ready: " + downwardRay.ToString() + "; Enabled: " + downwardRay.Enabled);
	}

    public override void _Input(InputEvent @event)
    {
         if (Input.IsActionPressed("selection"))
        {
			GD.Print("Camera position:" + GlobalTransform.Origin.ToString());
			GD.Print("Target position: " + downwardRay.TargetPosition.ToString());

            if (detectedCollider != null)
            {
                GD.Print("Selected object is: " + detectedCollider.ToString());
            }
        }
    }

    public override void _Process(double delta)
	{
		Vector3 camera_position = GlobalTransform.Origin;
		Vector3 downward_direction = new Vector3(0, -1, 0); // Vector pointing downward
		downwardRay.TargetPosition = camera_position + downward_direction * 30;
		downwardRay.ForceRaycastUpdate();
		GodotObject detectedCollider = downwardRay.GetCollider();
	}
}

On start up, I see this message as expected:

Downward ray ready: <RayCast3D#28991030576>; Enabled: True
Tile position:(0, 0, 0)

The marker is at the centre of the tile:

When triggering ‘selection’ the message:

Camera position:(0, 20, 0)
Target position: (0, -10, 0)

But the collider is not detected. I can see that the tile is at (0,0,0), and the ray is passing between (0,20,0) and (0,-10,0) so why isn’t it reporting the collision?

Just checking, but have you enabled collide_with_areas on your RayCast3D? By default that’s turned off, which has tripped me up before.

It wasn’t, but toggling it on didn’t make a difference. I expect it should detect the Tile without this because the tile has a CollisionBody3D node.

Area3Ds and PhysicsBody3Ds both use CollisionShape3Ds, but by default RayCast3D only detects PhysicsBody3Ds, like RigidBody3D, CharacterBody3D, and StaticBody3D.

I looked at your code properly and think it’s an issue of local space vs global space. target_position is defined in local space of your RayCast3D but your camera is rotated and the RayCast3D inherits that rotation, so you need to convert between them. I’m presuming you generally want the ray to be cast in the direction the camera is facing, so you instead of adding 30 units in the down direction you can just use the forward direction, which should be Vector3(0, 0, -1) IIRC.

Thanks!