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 ([email protected]())
{
_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;
}
}