Why isn't my Raycast2D visibility method working?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By duke_meister

I spent quite a while experimenting with a simple line of sight function using Raycast2D and finally got something working as needed (I hope to post it somewhere because I couldn’t find any tutorial that did what I needed).

Basically I have a top down RPG where I hide enemies that aren’t visible to the active PC by firing a ray between them, possibly colliding with tilemap tiles in between. All good.

Then I needed something simple that I thought would take me 10 minutes and has eluded me all afternoon. So, the player can click on the tile they want to move to. I only allow them to move there if the tile is ‘visible’ (no wall tiles in the way). I thought I’d use the above method by creating on-the-fly a fake enemy, put it on that tile then use the already working line of sight function to see if it’s visible.

It’s just not working. The ray does not collide with the newly placed enemy. I suspect a timing issue, but I can’t nail it down. I do use ForceRaycastUpdate (I’m using C# BTW). I also turn on collision shapes in Debug, and can see the ray is visually intersecting with the collision shape of the enemy…

I can post some code if need be. Any thoughts appreciated.

edit: PS I’m happy to replace my method with something better if need be.

edit: added link to sample project:
https://dropbox.com/s/rhp6s5mal18hhik/TestRaycast.zip?dl=0

The project has 3 pink skulls (enemies), a blue skull (player) and a ‘wall’. There is a timer that when it fires, it casts a ray from the player to an enemy. If it collides, the enemy remains visible. If it doesn’t collide with the enemy (hits the wall instead), it becomes invisible. Every timer event, the next enemy is checked. The timer is to slow things down.

What’s not working is that when you click the mouse, it should decide whether that position is visible to the player or not. If it is it moves the player there. If not, it doesn’t. To check this I instance an enemy node, use it for the visibility check, then get rid of it. It doesn’t ever collide with the node in this sample.

edit: added main.cs code (slightly different from what’s in the zip) for those who can’t get the cs files. See comment below also.

using Godot;
using System;
using System.Collections.Generic;
using System.Linq;

public class Main : Node2D
{
    private Player _player;
    private Enemy _enemy;
    private Enemy _enemy2;
    private Enemy _enemy3;
    private Enemy _visibilityNode;
    private List<Enemy> _enemies;
    private Timer _timer;
    private int _enemyNum;
    private RayCast2D _ray;
    private bool _mouseButtonPressed;
    private Wall _wall;
    private Enemy _currEnemy;
    private PackedScene _enemyScene;

    public override void _Ready()
    {
        _player = (Player)FindNode("Player");
        _enemy = (Enemy)FindNode("Enemy");
        _enemy.SetName("Enemy1");
        _enemy2 = (Enemy)FindNode("Enemy2");
        _enemy2.SetName("Enemy2");
        _enemy3 = (Enemy)FindNode("Enemy3");
        _enemy3.SetName("Enemy3");

        _enemyScene = (PackedScene)ResourceLoader.Load("res://Enemy.tscn");

        _enemies = new List<Enemy>{ _enemy3, _enemy2, _enemy};

        _wall = (Wall)FindNode("Wall");

        _ray = _player?.FindNode("RayCast2D") as RayCast2D;

        // using a timer so I can slow everything way down and see what's happening
        _timer = FindNode("Timer") as Timer;
        _timer.SetOneShot(false);
        _timer.SetWaitTime(1.0f); // change timeout here
        _timer.Connect("timeout", this, nameof(TimeoutFunc));
        _timer.Start();
    }

    public void TimeoutFunc()
    {
        if (_visibilityNode != null)
        {
            var collisionShape2D = _visibilityNode.FindNode("CollisionShape2D") as CollisionShape2D;
            collisionShape2D.SetDisabled(true);
        }

        _currEnemy = _enemies.ElementAt(_enemyNum);
        var enemySprite = _currEnemy.FindNode("Sprite") as Sprite;
        if (IsNull(enemySprite, "enemySprite")) throw new Exception();

        enemySprite?.SetVisible(EnemyIsVisible(_currEnemy, false));

        _enemyNum = (_enemyNum + 1) % _enemies.Count;
    }

    public override void _Input(InputEvent @event)
    {
        if (@event is InputEventMouseButton && !_mouseButtonPressed)
        {
            _mouseButtonPressed = true;
            if (PositionIsVisible(GetGlobalMousePosition()))
            {
                _player.SetGlobalPosition(GetGlobalMousePosition());
            }
            else
            {
                GD.Print("can't move there");
            }
        }
        else if (!@event.IsPressed())
        {
            _mouseButtonPressed = false;
        }
    }

    private bool PositionIsVisible(Vector2 vector2)
    {
        // set up a node to use for visibility check
        _visibilityNode = _enemyScene.Instance() as Enemy;
        if (IsNull(_visibilityNode, "_visibilityNode")) return false;

        AddChild(_visibilityNode);
        _visibilityNode.SetName("_visibilityNode");
        _visibilityNode.SetPosition(vector2);
        // only setting to visible for debugging
        _visibilityNode.SetVisible(true);

        // return whether the node is visible
        var nodeIsVisible = EnemyIsVisible(_visibilityNode, true);

        return nodeIsVisible;
    }

    private static bool IsNull(object o, string str)
    {
        if (o == null)
        {
            GD.Print($"{str ?? "object"} is null");
        }

        return o == null;
    }

    private bool EnemyIsVisible(Node2D enemyNodeToCheck, bool debug)
    {
        if (debug) GD.Print($"checking enemy {enemyNodeToCheck}{enemyNodeToCheck.GetInstanceId()} is visible");
        var mIsVisible = false;

        // only want to collide with given node or wall
        _ray.ClearExceptions();
        _enemies.ForEach(enemy => _ray.AddException(enemy));
        _ray.RemoveException(enemyNodeToCheck);
        _ray.AddException(_player); // just in case even though Exclude Parent is enabled

        // cast ray to node's position and force update
        var enemyPos = new Vector2(enemyNodeToCheck.GlobalPosition.x, enemyNodeToCheck.GlobalPosition.y);
        _ray?.SetCastTo(enemyPos - _ray.GlobalPosition);
        _ray?.ForceRaycastUpdate();

        // if we're colliding with something, could be the node or the wall
        if (_ray?.IsColliding() == true)
        {
            var collider = _ray.GetCollider() as Node2D;
            if (IsNull(collider, "collider")) throw new Exception();
            if (debug) GD.Print($"Is Colliding with {collider}{collider.GetInstanceId()}");

            // if we're colliding with an Enemy, must be the one we're checking for
            if (collider is Enemy)
            {
                mIsVisible = true;
            }
        }
        else
        {
            if (debug) GD.Print("not colliding");
        }

        if (debug) GD.Print($"{enemyNodeToCheck}{enemyNodeToCheck.GetInstanceId()} {(mIsVisible ? "" : "not")} visible");

        return mIsVisible;
    }
}

I opened your project but the cs files are missing. When opening it tells me there is a missing dependency (Wall.cs, Player.cs …)

metin | 2018-06-30 16:27

That’s weird, they all seem to be there when I open the zip in that dropbox link.
edit: player.cs, enemy.cs and wall.cs are all ‘default’. Just add a C# script to each of those scenes. I’ll add the main.cs above.

duke_meister | 2018-07-01 00:02

Ok, my mistake was that I didn’t open your project in the Mono version. However when I run your project with the most recent Mono version, Godot crashes and console says

Got a SIGABRT while executing native code.

metin | 2018-07-01 20:40

damn. I have no idea, it runs for me. I’m using 3.0.2 stable. Maybe I’ll make a gdscript version if it’s something to do with C#. Thanks for trying.

duke_meister | 2018-07-01 22:12

IMO creating a fake enemy just for a visibility check is very, very error-prone. There is a node called Point2D that is literally just a point in 2d. You can cast a ray to that point2d and see if it collides with a wall along the way.

selamba | 2019-12-05 00:35