diff --git a/addons/matcha/Constants.gd b/addons/matcha/Constants.gd new file mode 100644 index 0000000..9fcdffe --- /dev/null +++ b/addons/matcha/Constants.gd @@ -0,0 +1 @@ +const VERSION = "0.3.0" diff --git a/addons/matcha/MatchaLobby.gd b/addons/matcha/MatchaLobby.gd new file mode 100644 index 0000000..cd195da --- /dev/null +++ b/addons/matcha/MatchaLobby.gd @@ -0,0 +1,155 @@ +class_name MatchaLobby extends RefCounted + +# Signals +signal joined_room(room: MatchaRoom) +signal left_room(room: MatchaRoom) +signal room_created(room: Dictionary) +signal room_updated(room: Dictionary) +signal room_closed(room: Dictionary) + +# Members +var _lobby: MatchaRoom +var _rooms := {} +var _current_room: MatchaRoom + +# Getters +var room_list: + get: return _rooms.values() +var current_room: + get: return _current_room + +# Constructor +func _init(options:={}): + if not "identifier" in options: options.identifier = "com.matcha.lobby" + _lobby = MatchaRoom.create_mesh_room(options) + _lobby.peer_joined.connect(self._on_peer_joined) + _lobby.peer_left.connect(self._on_peer_left) + +# Public methods +func join_room(room_id: String) -> Error: + var room = _find_room_by_id(room_id) + if room == null: + push_error("Room not found") + return Error.ERR_DOES_NOT_EXIST + if _current_room != null: + push_error("Already in a room") + return Error.ERR_ALREADY_IN_USE + + _current_room = MatchaRoom.create_client_room(room_id) + joined_room.emit(_current_room) + + return Error.OK + +func create_room(room_meta:={}) -> Error: + if _current_room != null: + push_error("Already in a room") + return Error.ERR_ALREADY_IN_USE + if _lobby.peer_id in _rooms: + push_error("Already opened a room. Close it first!") + return Error.ERR_ALREADY_IN_USE + + _current_room = MatchaRoom.create_server_room() + var room = { + "id": _current_room.id, + "meta": room_meta + } + _rooms[_lobby.peer_id] = room + + _lobby.send_event("create_room", [room]) + room_created.emit(room) + joined_room.emit(_current_room) + + return Error.OK + +func update_room(room_meta:={}) -> Error: + if not _lobby.peer_id in _rooms: + push_error("No room opened.") + return Error.ERR_DOES_NOT_EXIST + + var room = _rooms[_lobby.peer_id] + room.meta = room_meta + _lobby.send_event("update_room", [room.meta]) + room_updated.emit(room) + + return Error.OK + +func leave_room() -> Error: + if _current_room == null: + push_error("Not in a room.") + return Error.ERR_DOES_NOT_EXIST + + if _lobby.peer_id in _rooms: + var room = _rooms[_lobby.peer_id] + _rooms.erase(_lobby.peer_id) + _lobby.send_event("close_room") + room_closed.emit(room) + + var previous_room = _current_room + _current_room = null + left_room.emit(previous_room) + + return Error.OK + +# Private methods +func _find_room_by_id(room_id: String): + for room: Dictionary in _rooms.values(): + if room.id == room_id: return room + return null + +func _verify_room(room: Dictionary) -> bool: + if typeof(room) != TYPE_DICTIONARY: return false + if not "id" in room or not "meta" in room: return false + if typeof(room.id) != TYPE_STRING or not _verify_room_meta(room.meta): return false + return true + +func _verify_room_meta(room_meta: Dictionary) -> bool: + return typeof(room_meta) == TYPE_DICTIONARY + +func _remove_room(room_id: String) -> bool: + for peer_id: String in _rooms.keys(): + var room: Dictionary = _rooms[peer_id] + if room.id != room_id: continue + + _rooms.erase(peer_id) + if peer_id == _lobby.peer_id: + _lobby.send_event("close_room") + room_closed.emit(room) + return true + + return false + +# Peer callbacks +func _on_peer_joined(id: int, peer: MatchaPeer) -> void: + peer.on_event("create_room", self._on_peer_created_room.bind(peer)) + peer.on_event("update_room", self._on_peer_updated_room.bind(peer)) + peer.on_event("close_room", self._on_peer_closed_room.bind(peer)) + + if _lobby.peer_id in _rooms: + peer.send_event("create_room", [_rooms[_lobby.peer_id]]) + +func _on_peer_left(_rpc_id: int, peer: MatchaPeer) -> void: + if peer.id in _rooms: + var room = _rooms[peer.id] + _rooms.erase(peer.id) + room_closed.emit(room) + +# Peer events +func _on_peer_created_room(room, peer: MatchaPeer) -> void: + if peer.id in _rooms: return + if not _verify_room(room): return + + _rooms[peer.id] = room + room_created.emit(room) + +func _on_peer_updated_room(room_meta: Dictionary, peer: MatchaPeer) -> void: + if not peer.id in _rooms: return + if not _verify_room_meta(room_meta): return + + _rooms[peer.id].meta = room_meta + room_updated.emit(_rooms[peer.id]) + +func _on_peer_closed_room(peer: MatchaPeer) -> void: + if peer.id in _rooms: + var room = _rooms[peer.id] + _rooms.erase(peer.id) + room_closed.emit(room) diff --git a/addons/matcha/MatchaPeer.gd b/addons/matcha/MatchaPeer.gd index 0835be6..048e3d6 100644 --- a/addons/matcha/MatchaPeer.gd +++ b/addons/matcha/MatchaPeer.gd @@ -15,7 +15,7 @@ signal sdp_created(sdp: String) # Members var _announced := false -var _peer_id: String +var _id: String var _offer_id: String var _state := State.NEW var _answered := false @@ -29,8 +29,6 @@ var is_connected: get: return _state == State.CONNECTED var type: get: return _type -var offer_id: - get: return _offer_id var gathered: get: return _state > State.GATHERING var announced: @@ -39,8 +37,12 @@ var answered: get: return _answered var local_sdp: get: return _local_sdp -var peer_id: - get: return _peer_id +var id: + get: return _id + set(value): _id = value +var offer_id: + get: return _offer_id + set(value): _offer_id = value # Static methods static func create_offer_peer(offer_id := Utils.gen_id()) -> MatchaPeer: @@ -92,12 +94,6 @@ func start() -> Error: Engine.get_main_loop().process_frame.connect(self.__poll) # Start the poll loop return Error.OK -func set_peer_id(new_peer_id: String) -> void: - _peer_id = new_peer_id - -func set_offer_id(new_offer_id: String) -> void: - _offer_id = new_offer_id - func set_answer(remote_sdp: String) -> Error: if _type != "offer": push_error("The peer is not an offer") diff --git a/addons/matcha/MatchaRoom.gd b/addons/matcha/MatchaRoom.gd index c76f841..2159b73 100644 --- a/addons/matcha/MatchaRoom.gd +++ b/addons/matcha/MatchaRoom.gd @@ -16,11 +16,11 @@ signal peer_left(rpc_id: int, peer: MatchaPeer) # Emitted when a peer left the r var _state := State.NEW # Internal state var _tracker_urls := [] # A list of tracker urls var _tracker_clients: Array[TrackerClient] = [] # A list of tracker clients we use to share/get offers/answers -var _room_id: String # An unique identifier +var _id: String # An unique id for this room var _peer_id := Utils.gen_id() var _type: String -var _offer_timeout := 30 -var _pool_size := 10 +var _offer_timeout := 120 +var _pool_size := 40 var _connected_peers = {} # Getters @@ -30,9 +30,11 @@ var peer_id: get: return _peer_id var type: get: return _type -var room_id: - get: return _room_id -var _peers: +var id: + get: return _id +var connected_peers: + get: return _connected_peers.values() +var peers: get: return get_peers().values().map(func(v): return v.connection) # Static methods @@ -54,14 +56,14 @@ func _init(options:={}): if not "pool_size" in options: options.pool_size = _pool_size if not "offer_timeout" in options: options.offer_timeout = _offer_timeout if not "identifier" in options: options.identifier = "com.matcha.default" - if not "tracker_urls" in options: options.tracker_urls = ["wss://tracker.webtorrent.dev"] + if not "tracker_urls" in options: options.tracker_urls = ["wss://tracker.openwebtorrent.com", "wss://tracker.files.fm:7073/announce"] if not "room_id" in options: options.room_id = options.identifier.sha1_text().substr(0, 20) if not "type" in options: options.type = "mesh" if not "autostart" in options: options.autostart = true _tracker_urls = options.tracker_urls _pool_size = options.pool_size _offer_timeout = options.offer_timeout - _room_id = options.room_id + _id = options.room_id _type = options.type peer_connected.connect(self._on_peer_connected) @@ -89,7 +91,7 @@ func start() -> Error: push_error("Creating client failed") return err elif _type == "server": - _room_id = _peer_id # Our room_id should be our peer_id to identify ourself as the server + _id = _peer_id # Our room_id should be our peer_id to identify ourself as the server var err := create_server() if err != OK: push_error("Creating server failed") @@ -111,7 +113,7 @@ func start() -> Error: func find_peers(filter:={}) -> Array[MatchaPeer]: var result: Array[MatchaPeer] = [] - for peer in _peers: + for peer in peers: var matched := true for key in filter: if not key in peer or peer[key] != filter[key]: @@ -129,9 +131,9 @@ func find_peer(filter:={}, allow_multiple_results:=false) -> MatchaPeer: # Broadcast an event to everybody in this room or just specific peers. (List of peer_id) func send_event(event_name: String, event_args:=[], target_peer_ids:=[]): - for peer: MatchaPeer in _peers: + for peer: MatchaPeer in peers: if not peer.is_connected: continue - if target_peer_ids.size() > 0 and not target_peer_ids.has(peer.peer_id): continue + if target_peer_ids.size() > 0 and not target_peer_ids.has(peer.id): continue peer.send_event(event_name, event_args) # Private methods @@ -189,19 +191,19 @@ func _handle_offers_announcment(): offer_peer.mark_as_announced() for tracker_client in _tracker_clients: # Announce the offers via every tracker - tracker_client.announce(_room_id, announce_offers) + tracker_client.announce(_id, announce_offers) func _send_answer_sdp(answer_sdp: String, peer: MatchaPeer, tracker_client: TrackerClient): - tracker_client.answer(_room_id, peer.peer_id, peer.offer_id, answer_sdp) + tracker_client.answer(_id, peer.id, peer.offer_id, answer_sdp) func _on_got_offer(offer: TrackerClient.Response, tracker_client: TrackerClient) -> void: - if offer.info_hash != _room_id: return - if find_peer({ "peer_id": offer.peer_id }) != null: return # Ignore if the peer is already known - if _type == "client" and offer.peer_id != room_id: return # Ignore offers from others than host (in client mode) + if offer.info_hash != _id: return + if find_peer({ "id": offer.peer_id }) != null: return # Ignore if the peer is already known + if _type == "client" and offer.peer_id != _id: return # Ignore offers from others than host (in client mode) var answer_peer := MatchaPeer.create_answer_peer(offer.offer_id, offer.sdp) var answer_rpc_id := 1 if _type == "client" else generate_unique_id() - answer_peer.set_peer_id(offer.peer_id) + answer_peer.id = offer.peer_id answer_peer.sdp_created.connect(self._send_answer_sdp.bind(answer_peer, tracker_client)) add_peer(answer_peer, answer_rpc_id) @@ -210,30 +212,30 @@ func _on_got_offer(offer: TrackerClient.Response, tracker_client: TrackerClient) remove_peer(answer_rpc_id) func _on_got_answer(answer: TrackerClient.Response, tracker_client: TrackerClient) -> void: - if answer.info_hash != _room_id: return - if _type == "client" and answer.peer_id != room_id: return # As client we just accept answers from the host + if answer.info_hash != _id: return + if _type == "client" and answer.peer_id != _id: return # As client we just accept answers from the host var offer_peer: MatchaPeer if _type == "client": if has_peer(1): offer_peer = get_peer(1).connection - offer_peer.set_offer_id(answer.offer_id) # Fix the offer_id since we gave the server alot of offers to choose from + offer_peer.offer_id = answer.offer_id # Fix the offer_id since we gave the server alot of offers to choose from else: offer_peer = find_peer({ "offer_id": answer.offer_id }) if offer_peer == null: return # Ignore if we dont know that offer - offer_peer.set_peer_id(answer.peer_id) + offer_peer.id = answer.peer_id offer_peer.set_answer(answer.sdp) func _on_failure(reason: String, tracker_client: TrackerClient) -> void: print("Tracker failure: ", reason, ", Tracker: ", tracker_client.tracker_url) -func _on_peer_connected(id: int): - var peer: MatchaPeer = get_peer(id).connection - _connected_peers[id] = peer - peer_joined.emit(id, peer) +func _on_peer_connected(rpc_id: int): + var peer: MatchaPeer = get_peer(rpc_id).connection + _connected_peers[rpc_id] = peer + peer_joined.emit(rpc_id, peer) -func _on_peer_disconnected(id: int): - var peer: MatchaPeer = _connected_peers[id] - _connected_peers.erase(id) - peer_left.emit(id, peer) +func _on_peer_disconnected(rpc_id: int): + var peer: MatchaPeer = _connected_peers[rpc_id] + _connected_peers.erase(rpc_id) + peer_left.emit(rpc_id, peer) diff --git a/addons/matcha/lib/WebSocketClient.gd b/addons/matcha/lib/WebSocketClient.gd index eb9b40f..17c2ade 100644 --- a/addons/matcha/lib/WebSocketClient.gd +++ b/addons/matcha/lib/WebSocketClient.gd @@ -1,6 +1,7 @@ # TODO: DOCUMENT, DOCUMENT, DOCUMENT! extends RefCounted +const Constants := preload("../Constants.gd") # Signals signal disconnected @@ -32,7 +33,8 @@ func _init(url: String, options:={}) -> void: _url = url _options = options - _user_agent = "Matcha/0.0.0 (%s; %s; %s) Godot/%s" % [ + _user_agent = "Matcha/%s (%s; %s; %s) Godot/%s" % [ + Constants.VERSION, OS.get_name(), OS.get_version(), Engine.get_architecture_name(), @@ -88,6 +90,7 @@ func close(was_error=false) -> void: _reconnect_try_counter += 1 if _reconnect_try_counter > _options.reconnect_tries: + push_error("Websocket retries exceeded") return _state = State.RECONNECTING diff --git a/examples/bobble/bobble.gd b/examples/bobble/bobble.gd index 5984a43..9c901ad 100644 --- a/examples/bobble/bobble.gd +++ b/examples/bobble/bobble.gd @@ -34,21 +34,21 @@ func _remove_player(peer_id: String) -> void: $Players.remove_child($Players.get_node(peer_id)) # Peer callbacks -func _on_peer_joined(id: int, peer: MatchaPeer) -> void: +func _on_peer_joined(peer_rpc_id: int, peer: MatchaPeer) -> void: # Listen to events the other peer may send peer.on_event("chat", self._on_peer_chat.bind(peer)) peer.on_event("secret", self._on_peer_secret.bind(peer)) - _add_player(peer.peer_id, id) # Create the player + _add_player(peer.id, peer_rpc_id) # Create the player -func _on_peer_left(_id: int, peer: MatchaPeer) -> void: - _remove_player(peer.peer_id) +func _on_peer_left(_peer_rpc_id: int, peer: MatchaPeer) -> void: + _remove_player(peer.id) func _on_peer_chat(message: String, peer: MatchaPeer) -> void: - $UI/chat_history.text += "\n%s: %s" % [peer.peer_id, message] - players[peer.peer_id].set_message(message) + $UI/chat_history.text += "\n%s: %s" % [peer.id, message] + players[peer.id].set_message(message) func _on_peer_secret(peer: MatchaPeer) -> void: - var sprite: Sprite2D = players[peer.peer_id].get_node("Sprite2D") + var sprite: Sprite2D = players[peer.id].get_node("Sprite2D") sprite.modulate = Color.from_hsv((randi() % 12) / 12.0, 1, 1) # UI Callbacks diff --git a/examples/lobby/lobby.gd b/examples/lobby/lobby.gd index 3eb6c96..e4671a6 100644 --- a/examples/lobby/lobby.gd +++ b/examples/lobby/lobby.gd @@ -1,2 +1,89 @@ extends Node2D +@onready var _room_list: ItemList = $room_list +var _lobby := MatchaLobby.new({ "identifier": "com.matcha.examples.lobby" }) +var _selected_room + +func _init(): + _lobby.joined_room.connect(self._on_joined_room) + _lobby.left_room.connect(self._on_left_room) + _lobby.room_created.connect(self._on_room_created) + _lobby.room_updated.connect(self._on_room_updated) + _lobby.room_closed.connect(self._on_room_closed) + +#func _ready(): +# lobby.create_room({ "name": "Penis" }) + +# Private methods + +# Callbacks +func _on_joined_room(room: MatchaRoom): + room.peer_joined.connect(self._on_peer_joined_room) + room.peer_left.connect(self._on_peer_left_room) + $room_join_btn.disabled = true + $room_create_btn.disabled = true + $current_room/room_log.text = "You joined the room: %s\n" % [room.id] + $current_room/room_leave_btn.disabled = false + +func _on_left_room(_room: MatchaRoom): + $current_room/room_log.text += "You left the room\n" + $current_room/room_leave_btn.disabled = true + $room_join_btn.disabled = false + $room_create_btn.disabled = false + +func _on_room_created(room: Dictionary) -> void: + var room_name = "Unnamed room (%s)" % [room.id] + if "name" in room.meta and room.meta.name != "": + room_name = room.meta.name + + var index := _room_list.add_item(room_name) + _room_list.set_item_metadata(index, room) + +func _on_room_updated(room: Dictionary) -> void: + for i in _room_list.item_count: + var list_room = _room_list.get_item_metadata(i) + if list_room.id != room.id: continue + _room_list.set_item_metadata(i, room) + + if "name" in room.meta and room.meta.name != "": + _room_list.set_item_text(i, room.meta.name) + + return + +func _on_room_closed(room: Dictionary) -> void: + for i in _room_list.item_count: + var list_room = _room_list.get_item_metadata(i) + if list_room.id != room.id: continue + _room_list.remove_item(i) + return + +func _on_peer_joined_room(_rpc_id: int, peer: MatchaPeer): + $current_room/room_log.text += "Peer joined the room (id: %s)\n" % [peer.id] + +func _on_peer_left_room(_rpc_id: int, peer: MatchaPeer): + $current_room/room_log.text += "Peer left the room (id: %s)\n" % [peer.id] + +# UI Callbacks +func _on_room_create_btn_pressed() -> void: + if _lobby.current_room != null: return + _lobby.create_room({ "name": $room_name_edit.text }) + +func _on_room_list_item_selected(index: int): + if _lobby.current_room != null: return + _selected_room = _room_list.get_item_metadata(index) + $room_join_btn.disabled = false + +func _on_room_list_empty_clicked(_at_position, _mouse_button_index): + if _lobby.current_room != null: return + _room_list.deselect_all() + _selected_room = null + $room_join_btn.disabled = true + +func _on_room_leave_btn_pressed(): + if _lobby.current_room == null: return + _lobby.leave_room() + +func _on_room_join_btn_pressed(): + if _lobby.current_room != null: return + if _selected_room == null: return + _lobby.join_room(_selected_room.id) diff --git a/examples/lobby/lobby.tscn b/examples/lobby/lobby.tscn index 736ff0e..acc2299 100644 --- a/examples/lobby/lobby.tscn +++ b/examples/lobby/lobby.tscn @@ -4,3 +4,76 @@ [node name="Lobby" type="Node2D"] script = ExtResource("1_vobbu") + +[node name="room_label" type="Label" parent="."] +offset_left = 234.0 +offset_top = 20.0 +offset_right = 307.0 +offset_bottom = 43.0 +text = "Room list" + +[node name="room_list" type="ItemList" parent="."] +offset_left = 31.0 +offset_top = 55.0 +offset_right = 509.0 +offset_bottom = 663.0 + +[node name="room_name_edit" type="TextEdit" parent="."] +offset_left = 32.0 +offset_top = 679.0 +offset_right = 430.0 +offset_bottom = 711.0 +placeholder_text = "Room name" + +[node name="room_create_btn" type="Button" parent="."] +offset_left = 436.0 +offset_top = 680.0 +offset_right = 507.0 +offset_bottom = 711.0 +text = "Create" + +[node name="room_join_btn" type="Button" parent="."] +offset_left = 32.0 +offset_top = 721.0 +offset_right = 184.0 +offset_bottom = 752.0 +disabled = true +text = "Join selected room" + +[node name="current_room" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 0 +offset_left = 611.0 +offset_right = 1197.0 +offset_bottom = 798.0 + +[node name="room_label" type="Label" parent="current_room"] +layout_mode = 2 +offset_left = 250.0 +offset_top = 23.0 +offset_right = 356.0 +offset_bottom = 46.0 +text = "Current room" + +[node name="room_log" type="RichTextLabel" parent="current_room"] +layout_mode = 0 +offset_left = 25.0 +offset_top = 75.0 +offset_right = 530.0 +offset_bottom = 707.0 +scroll_following = true + +[node name="room_leave_btn" type="Button" parent="current_room"] +layout_mode = 0 +offset_left = 468.0 +offset_top = 722.0 +offset_right = 521.0 +offset_bottom = 753.0 +disabled = true +text = "Leave" + +[connection signal="empty_clicked" from="room_list" to="." method="_on_room_list_empty_clicked"] +[connection signal="item_selected" from="room_list" to="." method="_on_room_list_item_selected"] +[connection signal="pressed" from="room_create_btn" to="." method="_on_room_create_btn_pressed"] +[connection signal="pressed" from="room_join_btn" to="." method="_on_room_join_btn_pressed"] +[connection signal="pressed" from="current_room/room_leave_btn" to="." method="_on_room_leave_btn_pressed"] diff --git a/examples/server_client/server_client.gd b/examples/server_client/server_client.gd index 4245aa1..e41d75b 100644 --- a/examples/server_client/server_client.gd +++ b/examples/server_client/server_client.gd @@ -6,16 +6,16 @@ var client_room: MatchaRoom func _on_start_server_pressed(): $start_server.disabled = true server_room = MatchaRoom.create_server_room() - $server_roomid_edit.text = server_room.room_id - $client_roomid_edit.text = server_room.room_id + $server_roomid_edit.text = server_room.id + $client_roomid_edit.text = server_room.id $start_client.disabled = false - $logs.text += "[Server] Joined (room_id=%s)\n" % [server_room.room_id] + $logs.text += "[Server] Joined (room_id=%s)\n" % [server_room.id] server_room.peer_joined.connect(func(_id: int, peer: MatchaPeer): - $logs.text += "[Server] Peer joined (peer_id=%s)\n" % [peer.peer_id] + $logs.text += "[Server] Peer joined (id=%s)\n" % [peer.id] ) server_room.peer_left.connect(func(_id: int, peer: MatchaPeer): - $logs.text += "[Server] Peer left (peer_id=%s)\n" % [peer.peer_id] + $logs.text += "[Server] Peer left (id=%s)\n" % [peer.id] ) func _on_start_client_pressed(): @@ -25,10 +25,10 @@ func _on_start_client_pressed(): $logs.text += "[Client] Joined (room_id=%s)\n" % [$client_roomid_edit.text] client_room.peer_joined.connect(func(_id: int, peer: MatchaPeer): - $logs.text += "[Client] Peer joined (peer_id=%s)\n" % [peer.peer_id] + $logs.text += "[Client] Peer joined (id=%s)\n" % [peer.id] ) client_room.peer_left.connect(func(_id: int, peer: MatchaPeer): - $logs.text += "[Client] Peer left (peer_id=%s)\n" % [peer.peer_id] + $logs.text += "[Client] Peer left (id=%s)\n" % [peer.id] ) func _on_client_roomid_edit_text_changed(new_text): diff --git a/root.tscn b/root.tscn index e0fb240..e91a7b9 100644 --- a/root.tscn +++ b/root.tscn @@ -5,28 +5,24 @@ [node name="root" type="Node2D"] script = ExtResource("1_5f6ns") -[node name="bobble_btn" type="Button" parent="."] -offset_left = 508.0 -offset_top = 208.0 -offset_right = 612.0 -offset_bottom = 239.0 -text = "Start bobble" - -[node name="lobby_btn" type="Button" parent="."] -visible = false -offset_left = 515.0 +[node name="VBoxContainer" type="VBoxContainer" parent="."] +offset_left = 492.0 offset_top = 267.0 -offset_right = 608.0 -offset_bottom = 298.0 -text = "Start lobby" +offset_right = 698.0 +offset_bottom = 428.0 + +[node name="bobble_btn" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Bobble (mesh)" + +[node name="lobby_btn" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Lobby" -[node name="server_client_btn" type="Button" parent="."] -offset_left = 506.0 -offset_top = 334.0 -offset_right = 623.0 -offset_bottom = 365.0 +[node name="server_client_btn" type="Button" parent="VBoxContainer"] +layout_mode = 2 text = "Server / Client" -[connection signal="pressed" from="bobble_btn" to="." method="_on_bobble_btn_pressed"] -[connection signal="pressed" from="lobby_btn" to="." method="_on_lobby_btn_pressed"] -[connection signal="pressed" from="server_client_btn" to="." method="_on_server_client_btn_pressed"] +[connection signal="pressed" from="VBoxContainer/bobble_btn" to="." method="_on_bobble_btn_pressed"] +[connection signal="pressed" from="VBoxContainer/lobby_btn" to="." method="_on_lobby_btn_pressed"] +[connection signal="pressed" from="VBoxContainer/server_client_btn" to="." method="_on_server_client_btn_pressed"]