edbeeching / godot_rl_agents

An Open Source package that allows video game creators, AI researchers and hobbyists the opportunity to learn complex behaviors for their Non Player Characters or agents
MIT License
942 stars 69 forks source link

Unable to add RayCastSemsor2D to node. #40

Closed Jetpackjules11 closed 1 year ago

Jetpackjules11 commented 1 year ago

I got the plugin working, but whenever I try to add RaycastSensor2D to my player node, I get this error:

Cannot get class 'ISensor2D'. editor/create_dialog.cpp:261 - Condition "inherits.is_empty()" is true. Cannot get class ''. editor/editor_data.cpp:551 - Parameter "p_object" is null. editor/scene_tree_dock.cpp:2184 - Condition "!child" is true.

It also looks a bit weird in the files system: image

edbeeching commented 1 year ago

How did you add the addon? Did you copy addon/godot_rl_agents to your project directory?

Jetpackjules11 commented 1 year ago

yep. I can see the sync node available to add and in the screenshot there is also RayCastSensor3D, which also comes with the plugin so it's def. working

Jetpackjules11 commented 1 year ago

image image

edbeeching commented 1 year ago

Hmm this is strange, because Godot does not seem to find ISensor2D. Can you open that file inside the editor and see if there are any errors?

Jetpackjules11 commented 1 year ago

It aactually shows up when I search for "sens"...

image

Jetpackjules11 commented 1 year ago

Hmm this is strange, because Godot does not seem to find ISensor2D. Can you open that file inside the editor and see if there are any errors?

The ISensor2D.gd file?

edbeeching commented 1 year ago

Yes

Jetpackjules11 commented 1 year ago

image

Jetpackjules11 commented 1 year ago

Also this for the RaycastSensor2D.gd

image

edbeeching commented 1 year ago

Ah this was a bug that appeared when I moved to Godot 4. I thought I fixed it, somewhere _obs is declared to be null. Did you copy the addon from another example or from the plugin folder?

Jetpackjules11 commented 1 year ago

plugin folder

edbeeching commented 1 year ago

Could you try copying it from the BallChase example and see if you have the same error?

Jetpackjules11 commented 1 year ago

Still this: image

and this: image image

edbeeching commented 1 year ago

Ok as a workaround you can probably remove the Array type annotation on that function. Change line 70 to func get_observation():

Jetpackjules11 commented 1 year ago

I will give that a shot. I need to go now, though. Will have more time later today

Get Outlook for iOShttps://aka.ms/o0ukef


From: Edward Beeching @.> Sent: Saturday, January 7, 2023 12:59:36 PM To: edbeeching/godot_rl_agents @.> Cc: Jetpackjules11 @.>; Author @.> Subject: Re: [edbeeching/godot_rl_agents] Unable to add RayCastSemsor2D to node. (Issue #40)

Ok as a workaround you can probably remove the Array type annotation on that function. Change line 70 to func get_observation():

— Reply to this email directly, view it on GitHubhttps://github.com/edbeeching/godot_rl_agents/issues/40#issuecomment-1374610583, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A27YGMBSDJGVSJYMML34B3TWRHKLRANCNFSM6AAAAAATUDWHAQ. You are receiving this because you authored the thread.Message ID: @.***>

Jetpackjules11 commented 1 year ago

Thanks for the help, that seemed to work!

Jetpackjules11 commented 1 year ago

I wm now encountering a new error... when I try to run gdrl on my custom environment, the raycast script returns this error: image

Could this be due to the change from array? Is it not picking up on a _obs variable somewhere else?

edbeeching commented 1 year ago

Which version of the Godot 4 beta are you using?

Jetpackjules commented 1 year ago

I got the beta10 win64.exe version.

from this website: https://downloads.tuxfamily.org/godotengine/4.0/

(Also I am same person, just had forgotten main accnt (this one's) password...

edbeeching commented 1 year ago

could you paste the entire contents of ISensor2D.gd and RaycastSensor2D.gd here, somewhere _obs is set to be null. In a previous version this was the case and I removed the null initialization. I have a feeling it creeped back in somewhere.

Jetpackjules commented 1 year ago

I don't have access to the computer I am using for my project right now but can back to you tonight.

Jetpackjules commented 1 year ago

Here is the I sensor 2d Code:

extends Node2D
class_name ISensor2D

var _obs : Array
var _active := false

func get_observation():
    pass

func activate():
    _active = true

func deactivate():
    _active = false

func _update_observation():
    pass

func reset():
    pass

And here is the RaycastSensor2D code:

extends ISensor2D
class_name RaycastSensor2D
@tool

@export var n_rays := 16.0:
    get: return n_rays
    set(value):
        n_rays = value
        _update()

@export var ray_length := 200:# (float,5,200,5.0)
    get: return ray_length
    set(value):
        ray_length = value
        _update()
@export var cone_width := 360.0:# (float,5,360,5.0)
    get: return cone_width
    set(value):
        cone_width = value
        _update()

@export var debug_draw := false :
    get: return debug_draw 
    set(value):
        debug_draw = value
        _update()  

var _angles = []
var rays := []

func _update():
    if Engine.is_editor_hint():
        _spawn_nodes()  

func _ready() -> void:
    _spawn_nodes()

func _spawn_nodes():
    for ray in rays:
        ray.queue_free()
    rays = []

    _angles = []
    var step = cone_width / (n_rays)
    var start = step/2 - cone_width/2

    for i in n_rays:
        var angle = start + i * step
        var ray = RayCast2D.new()
        ray.set_target_position(Vector2(
            ray_length*cos(deg_to_rad(angle)),
            ray_length*sin(deg_to_rad(angle))
        ))
        ray.set_name("node_"+str(i))
        ray.enabled  = true
        ray.collide_with_areas = true
        add_child(ray)
        rays.append(ray)

        _angles.append(start + i * step)

func _physics_process(delta: float) -> void:
    if self._active:
        self._obs = calculate_raycasts()

func get_observation():
    if len(self._obs) == 0:
        print("obs was null, forcing raycast update")
        return self.calculate_raycasts()
    return self._obs

func calculate_raycasts() -> Array:
    var result = []
    for ray in rays:
        ray.force_raycast_update()
        var distance = _get_raycast_distance(ray)
        result.append(distance)
    return result

func _get_raycast_distance(ray : RayCast2D) -> float : 
    if !ray.is_colliding():
        return 0.0

    var distance = (global_position - ray.get_collision_point()).length()
    distance = clamp(distance, 0.0, ray_length)
    return (ray_length - distance) / ray_length
edbeeching commented 1 year ago

Ok, I am a bit stumped by this one. Last try, could you paste the content of sync.gd ?

Jetpackjules commented 1 year ago

Sure thing:

extends Node
# --fixed-fps 2000 --disable-render-loop
@export var action_repeat := 8
@export var speed_up = 1
var n_action_steps = 0

const MAJOR_VERSION := "0"
const MINOR_VERSION := "3" 
const DEFAULT_PORT := "11008"
const DEFAULT_SEED := "1"
const DEFAULT_ACTION_REPEAT := "8"
var stream : StreamPeerTCP = null
var connected = false
var message_center
var should_connect = true
var agents
var need_to_send_obs = false
var args = null
@onready var start_time = Time.get_ticks_msec()
var initialized = false
var just_reset = false

# Called when the node enters the scene tree for the first time.

func _ready():

    await get_tree().root.ready
    get_tree().set_pause(true) 
    _initialize()
    await get_tree().create_timer(1.0).timeout
    get_tree().set_pause(false) 

func _get_agents():
    agents = get_tree().get_nodes_in_group("AGENT")

func _set_heuristic(heuristic):
    for agent in agents:
        agent.set_heuristic(heuristic)

func _handshake():
    print("performing handshake")

    var json_dict = _get_dict_json_message()
    assert(json_dict["type"] == "handshake")
    var major_version = json_dict["major_version"]
    var minor_version = json_dict["minor_version"]
    if major_version != MAJOR_VERSION:
        print("WARNING: major verison mismatch ", major_version, " ", MAJOR_VERSION)  
    if minor_version != MINOR_VERSION:
        print("WARNING: minor verison mismatch ", minor_version, " ", MINOR_VERSION)

    print("handshake complete")

func _get_dict_json_message():
    # returns a dictionary from of the most recent message
    # this is not waiting
    while stream.get_available_bytes() == 0:
        stream.poll()
        if stream.get_status() != 2:
            print("server disconnected status, closing")
            get_tree().quit()
            return null

        OS.delay_usec(10)

    var message = stream.get_string()
    var json_data = JSON.parse_string(message)

    return json_data

func _send_dict_as_json_message(dict):
    stream.put_string(JSON.stringify(dict))

func _send_env_info():
    var json_dict = _get_dict_json_message()
    assert(json_dict["type"] == "env_info")

    var message = {
        "type" : "env_info",
        #"obs_size": agents[0].get_obs_size(),
        "observation_space": agents[0].get_obs_space(),
        "action_space":agents[0].get_action_space(),
        "n_agents": len(agents)
        }
    _send_dict_as_json_message(message)

func connect_to_server():
    print("Waiting for one second to allow server to start")
    OS.delay_msec(1000)
    print("trying to connect to server")
    stream = StreamPeerTCP.new()

    # "localhost" was not working on windows VM, had to use the IP
    var ip = "127.0.0.1"
    var port = _get_port()
    var connect = stream.connect_to_host(ip, port)
    stream.set_no_delay(true) # TODO check if this improves performance or not
    stream.poll()
    return stream.get_status() == 2

func _get_args():
    print("getting command line arguments")
#   var arguments = {}
#   for argument in OS.get_cmdline_args():
#       # Parse valid command-line arguments into a dictionary
#       if argument.find("=") > -1:
#           var key_value = argument.split("=")
#           arguments[key_value[0].lstrip("--")] = key_value[1]

    var arguments = {}
    for argument in OS.get_cmdline_args():
        print(argument)
        if argument.find("=") > -1:
            var key_value = argument.split("=")
            arguments[key_value[0].lstrip("--")] = key_value[1]
        else:
            # Options without an argument will be present in the dictionary,
            # with the value set to an empty string.
            arguments[argument.lstrip("--")] = ""

    return arguments   

func _get_speedup():
    print(args)
    return args.get("speedup", str(speed_up)).to_int()

func _get_port():    
    return args.get("port", DEFAULT_PORT).to_int()

func _set_seed():
    var _seed = args.get("env_seed", DEFAULT_SEED).to_int()
    seed(_seed)

func _set_action_repeat():
    action_repeat = args.get("action_repeat", DEFAULT_ACTION_REPEAT).to_int()

func disconnect_from_server():
    stream.disconnect_from_host()

func _initialize():
    _get_agents()

    args = _get_args()
    Engine.physics_ticks_per_second = _get_speedup() * 60 # Replace with function body.
    Engine.time_scale = _get_speedup() * 1.0
    prints("physics ticks", Engine.physics_ticks_per_second, Engine.time_scale, _get_speedup(), speed_up)

    connected = connect_to_server()
    if connected:
        _set_heuristic("model")
        _handshake()
        _send_env_info()
    else:
        _set_heuristic("human")  

    _set_seed()
    _set_action_repeat()
    initialized = true  

func _physics_process(delta): 
    # two modes, human control, agent control
    # pause tree, send obs, get actions, set actions, unpause tree
    if n_action_steps % action_repeat != 0:
        n_action_steps += 1
        return

    n_action_steps += 1

    if connected:
        get_tree().set_pause(true) 

        if just_reset:
            just_reset = false
            var obs = _get_obs_from_agents()

            var reply = {
                "type": "reset",
                "obs": obs
            }
            _send_dict_as_json_message(reply)
            # this should go straight to getting the action and setting it checked the agent, no need to perform one phyics tick
            get_tree().set_pause(false) 
            return

        if need_to_send_obs:
            need_to_send_obs = false
            var reward = _get_reward_from_agents()
            var done = _get_done_from_agents()
            #_reset_agents_if_done() # this ensures the new observation is from the next env instance : NEEDS REFACTOR

            var obs = _get_obs_from_agents()

            var reply = {
                "type": "step",
                "obs": obs,
                "reward": reward,
                "done": done
            }
            _send_dict_as_json_message(reply)

        var handled = handle_message()
    else:
        _reset_agents_if_done()

func handle_message() -> bool:
    # get json message: reset, step, close
    var message = _get_dict_json_message()
    if message["type"] == "close":
        print("received close message, closing game")
        get_tree().quit()
        get_tree().set_pause(false) 
        return true

    if message["type"] == "reset":
        print("resetting all agents")
        _reset_all_agents()
        just_reset = true
        get_tree().set_pause(false) 
        #print("resetting forcing draw")
#        RenderingServer.force_draw()
#        var obs = _get_obs_from_agents()
#        print("obs ", obs)
#        var reply = {
#            "type": "reset",
#            "obs": obs
#        }
#        _send_dict_as_json_message(reply)   
        return true

    if message["type"] == "call":
        var method = message["method"]
        var returns = _call_method_on_agents(method)
        var reply = {
            "type": "call",
            "returns": returns
        }
        print("calling method from Python")
        _send_dict_as_json_message(reply)   
        return handle_message()

    if message["type"] == "action":
        var action = message["action"]
        _set_agent_actions(action) 
        need_to_send_obs = true
        get_tree().set_pause(false) 
        return true

    print("message was not handled")
    return false

func _call_method_on_agents(method):
    var returns = []
    for agent in agents:
        returns.append(agent.call(method))

    return returns

func _reset_agents_if_done():
    for agent in agents:
        if agent.get_done(): 
            agent.set_done_false()

func _reset_all_agents():
    for agent in agents:
        agent.needs_reset = true
        #agent.reset()   

func _get_obs_from_agents():
    var obs = []
    for agent in agents:
        obs.append(agent.get_obs())
    return obs

func _get_reward_from_agents():
    var rewards = [] 
    for agent in agents:
        rewards.append(agent.get_reward())
        agent.zero_reward()
    return rewards    

func _get_done_from_agents():
    var dones = [] 
    for agent in agents:
        var done = agent.get_done()
        if done: agent.set_done_false()
        dones.append(done)
    return dones    

func _set_agent_actions(actions):
    for i in range(len(actions)):
        agents[i].set_action(actions[i])
Jetpackjules commented 1 year ago

Could this be due to _physics_process() not being called before the self._obs call? (In raycast sensor 2d)

func _physics_process(delta: float) -> void:
    if self._active:
        self._obs = calculate_raycasts()
Jetpackjules commented 1 year ago

I figured it out!

The issue was that in the RayCastSensor2D script, the get_observation() function checked if the self._obs length was 0. It should have been checking if the actual variable was null.

So from this:

func get_observation():
    if len(self._obs) == 0:
        print("obs was null, forcing raycast update")
        return self.calculate_raycasts()
    return self._obs

to this:

func get_observation():
    if (self._obs == null):
        print("obs was null, forcing raycast update")
        return self.calculate_raycasts()
    return self._obs
edbeeching commented 1 year ago

Ah nice one! It is strange that we have not seen this bug in the other examples. The problem could also be to do with where you put your sync node in the scene tree. I think it should be the first child of the root. Either way, I am glad you have solved it. Let me know if there are other issues!