writing a tutorial for a chatroom in WebSocket
@ -0,0 +1,219 @@
|
|||||||
|
<!---->
|
||||||
|
<h1>Godot Tutorial: Chat Room using WebSocket </h1>
|
||||||
|
august 31, 2022<br>
|
||||||
|
#webdev <br>
|
||||||
|
<br>
|
||||||
|
<p>Follow along to make your first mini WebSocket application in GDScript (with a little JSON). The client and the server will be two separate projects. I build upon the <a href="http://www.narwalengineering.com/2018/07/01/godot-tutorial-simple-chat-room-using-multiplayer-api/">NetworkedMultiplayerENet chat room tutorial by Miziziziz</a>(dead link) and the <a href="https://docs.godotengine.org/en/stable/tutorials/networking/websocket.html">HTML5 and WebSocket tutorial in the Godot documentation</a>. </p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Why WebSocket over UDP? </h2>
|
||||||
|
<p>UDP is fast but inaccurate. It is best used for real-time action gameplay. </p>
|
||||||
|
<p>TCP is slow but accurate. It is best for sharing data. </p>
|
||||||
|
<p>In Godot, the TCP stuff is actually UDP. If you want TCP, you should use WebSocket. </p>
|
||||||
|
|
||||||
|
<h2>Do I need a dedicated server for testing? </h2>
|
||||||
|
<p>Nope! You can test client & server code on your own computer. I will test on both a single computer at home and a VPS on a server stationed in another country. </p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Make your projects. </h2>
|
||||||
|
<p>Make one for the server and one for the client. You can do almost everything in the engine. If you export your projects after changes, though, you can easily see debug information for both sides of the application and test multiple clients. Godot is fast and light for a game engine, so I don't think it's unreasonable to add an exporting step to your workflow. </p>
|
||||||
|
|
||||||
|
<p>I named my projects ChatServer and ChatClient. </p>
|
||||||
|
|
||||||
|
<h2>Setting up the server. </h2>
|
||||||
|
|
||||||
|
<h3>Node Hierarchy. </h3>
|
||||||
|
<p>One scene with a node is enough for the server. </p>
|
||||||
|
<img src="/static/img/ent/Chatbox_ServerHierarchy.png" alt="(Screenshot: Node hierarchy is just one node named Server.)"> <br>
|
||||||
|
|
||||||
|
<h3>Declare your variables. </h3>
|
||||||
|
<pre><code>
|
||||||
|
const PORT = 8080
|
||||||
|
var _server = WebSocketServer.new()
|
||||||
|
var peers = []
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p><code>PORT</code> is the port your applications will use. If you are having trouble connecting, try 9001, etc, until you find a free one. <code>peers</code> will let us access everyone currently connected to the server. </p>
|
||||||
|
|
||||||
|
<h3>Build your skeleton. </h3>
|
||||||
|
<pre><code>
|
||||||
|
func _ready():
|
||||||
|
_server.connect('client_connected', self, "_connected")
|
||||||
|
_server.connect('client_disconnected', self, "_disconnected")
|
||||||
|
_server.connect('client_close_request', self, "_close_request")
|
||||||
|
_server.connect("data_received", self, "_on_data_received")
|
||||||
|
|
||||||
|
var err = _server.listen(PORT)
|
||||||
|
if err != OK:
|
||||||
|
print_debug("Unable to start server")
|
||||||
|
set_process(false)
|
||||||
|
print_debug("server started")
|
||||||
|
|
||||||
|
func _connected(id, protocol):
|
||||||
|
print_debug("Client %d connected with protocol:
|
||||||
|
%s" % [id, protocol])
|
||||||
|
|
||||||
|
func _close_request(id, code, reason):
|
||||||
|
print_debug("Client %d disconnecting with code:
|
||||||
|
%d, reason: %s" % [id, code, reason])
|
||||||
|
|
||||||
|
func _disconnected(id, was_clean = false):
|
||||||
|
print_debug("Client %d disconnected, clean:
|
||||||
|
%s" % [id, str(was_clean)])
|
||||||
|
|
||||||
|
func _on_data_received(id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _process(_delta):
|
||||||
|
_server.poll()
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>The <code>_ready</code> method prepares the server to catch all the signals it needs for a simple application. <code>client_connected</code> triggers when a client connects, <code>client_disconnected</code> when one disconnects, <code>client_close_request</code> when one requests to close, and <code>data_received</code> when a packet is received. Fairly obvious. The next block of code starts up the server, or at least makes an attempt. </p>
|
||||||
|
|
||||||
|
<p>So long as the process is running, the server polls constantly. </p>
|
||||||
|
|
||||||
|
<p>Let's start the client before we get any further. </p>
|
||||||
|
|
||||||
|
<h2>Setting up the client. </h2>
|
||||||
|
|
||||||
|
<h3>Design work. </h3>
|
||||||
|
<p>The client requires both some basic functionality and a barebones UI. </p>
|
||||||
|
<p>To go for this look... </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_UI.png" alt="(Screenshot: The look of the UI - a title bar on top, a large TextEdit field for everyone's messages, and a LineEdit beneath for entering messages. If you are 'logged out,' a username field and join button will appear at the bottom.)">
|
||||||
|
|
||||||
|
<p>...we'll need this tree. </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_ClientHierarchy.png" alt="(Screenshot: Node hierarchy is a Node named Client a Panel Container child named UI. The UI has a VBoxContainer child containing a Label, TextEdit, LineEdit, and an HBoxContainer. The HBox contains a LineEdit and a button.)">
|
||||||
|
|
||||||
|
<p>The root node will hold the programmatic half of the client, while the UI node will hold all the code for displaying things to the user. </p>
|
||||||
|
|
||||||
|
<p>To make UIs, I like to block everything out in containers, especially VBoxContainers and HboxContainers. I made the UI a PanelContainer so everything lays on the default gray background (or any custom background, choosable in the Inspector as a StyleBoxTexture). </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_Stylebox.png" alt="(Screenshot: The PanelContainer Inspector>Theme Overrides>Styles>Panel>New StyleBoxTexture.)">
|
||||||
|
|
||||||
|
<p>Also in the Inspector, set the size (rect_size) to 250 by 600px. Make sure the Position is still (0,0). You can also match the window's size by going to the top-left menu>Project>Project Settings...>General>Display>Window>Size and set Width to 250 and Height to 600. </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_UI_rect_size.png" alt="(Screenshot: PanelContainer Inspector>Rect>Size...x = 250 and y = 600.)">
|
||||||
|
<img src="/static/img/ent/Chatbox_ClientProjectSettings.png" alt="(Screenshot: Project Settings.)">
|
||||||
|
|
||||||
|
<p>The layout is more vertical overall, so the first structural node is a VBoxContainer. Add it as a child, then go to Inspector>Size Flags. Check Fill and Expand for both Horizontal and Vertical, so it automatically fills the PanelContainer. When you check Fill and Expand, it will take up 100% of the space in that direction of its parent when it is the only child. If there are multiple children, each with Expand, they will evenly divide the space. If one child has fixed dimensions, it will stay that size, while all the Expand children evenly split up the remainder. </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_SizeFlags.png" alt="(Screenshot: VBoxContainer's Size Flags are checked for Horizontal Fill and Expand and Vertical Fill and Expand.)">
|
||||||
|
|
||||||
|
<p>For the Label, add the application's title in the Inspector's Text field, set Align to Center, and set the Size Flags to Horizontal Fill and Expand. </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_LabelSizeFlags.png" alt="(Screenshot: VBoxContainer's Size Flags are checked for Horizontal Fill and Expand and Vertical Fill and Expand.)">
|
||||||
|
|
||||||
|
<p>For the ChatLog (TextEdit), check the Show Line Numbers and set Size Flags to Fill and Expand for both Horizontal and Vertical. </p>
|
||||||
|
|
||||||
|
<p>For the ChatInput (LineEdit), under Placeholder, set the Text to "Enter your message..." and the Size Flags to Fill and Expand Horizontally. </p>
|
||||||
|
|
||||||
|
<p>The next section is laid out horizontally, so use an HBoxContainer. Add a LineEdit for the Username with "Username" as its Placeholder Text and the Horizontal Fill and Expand boxes checked. Also add a button with its Text set to Join and its Horizontal Fill and Expand boxes checked.</p>
|
||||||
|
|
||||||
|
<h3>Client Skeleton </h3>
|
||||||
|
|
||||||
|
<p>Here is the first sketch of the UI code. The Client is accessible as <code>get_parent()</code>, the important nodes as variables, and the signals related to entering a message and pressing the join button are handled programmatically. </p>
|
||||||
|
|
||||||
|
<pre><code>
|
||||||
|
UI.gd (Client/UI PanelContainer)
|
||||||
|
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
onready var chat_log = $VBox/ChatLog
|
||||||
|
onready var chat_input = $VBox/ChatInput
|
||||||
|
onready var join_button = $VBox/HBox/JoinButton
|
||||||
|
onready var username = $VBox/HBox/Name
|
||||||
|
|
||||||
|
func display_message(packet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
func join_chat():
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if event is InputEventKey:
|
||||||
|
if event.pressed and event.scancode == KEY_ENTER:
|
||||||
|
get_parent().send_message(chat_input.text)
|
||||||
|
chat_input.text = ""
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
join_button.connect("button_up", self, "join_chat")
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>UI always takes up so much space without actually doing that much...Let's block out the functionality on the root node's script. </p>
|
||||||
|
|
||||||
|
<pre><code>
|
||||||
|
Client.gd (Client Node)
|
||||||
|
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
export var websocket_url = "ws://127.0.0.1:8080"
|
||||||
|
var _client = WebSocketClient.new()
|
||||||
|
onready var UI = $UI
|
||||||
|
|
||||||
|
var chat_name
|
||||||
|
|
||||||
|
func join_chat(username):
|
||||||
|
pass
|
||||||
|
|
||||||
|
func send_message(message):
|
||||||
|
pass
|
||||||
|
|
||||||
|
sync func receive_message(packet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
_client.connect("connection_closed", self, "_closed")
|
||||||
|
_client.connect("connection_error", self, "_error")
|
||||||
|
_client.connect("connection_established", self, "_connected")
|
||||||
|
_client.connect("data_received", self, "_on_data_received")
|
||||||
|
|
||||||
|
var err = _client.connect_to_url(websocket_url)
|
||||||
|
if err != OK:
|
||||||
|
print_debug("Unable to connect")
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
|
func _error(was_clean = false):
|
||||||
|
print_debug("Error, clean: ", was_clean)
|
||||||
|
|
||||||
|
func _closed(was_clean = false):
|
||||||
|
print_debug("Closed, clean: ", was_clean)
|
||||||
|
|
||||||
|
func _connected(protocol = ""):
|
||||||
|
print_debug("Connected with protocol: ", protocol)
|
||||||
|
|
||||||
|
func _on_data_received():
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _process(_delta):
|
||||||
|
_client.poll()
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>It resembles the server. You can add a security protocol when you use wss:// instead of ws:/ for the websocket_url, but that is outside the scope of this tutorial. 127.0.0.1 loops back to your own computer, so you can use this for testing at home without involving an outside VPS, etc. Then, the port must match whatever you use for the server's <code>PORT</code> constant. 8080 ended up working for me. </p>
|
||||||
|
|
||||||
|
<p>And with that, we have enough to try it out. </p>
|
||||||
|
|
||||||
|
<h3>Export & Run </h3>
|
||||||
|
|
||||||
|
<p>Export just the server for now. If you want to try multiple clients, export the client, too, but there isn't a lot to test yet. To Export, go to the top-left menu>Project>Export... If you've been here before, choose the right presets for making a standalone application for your computer. Otherwise, on the top, click Add... then choose your operating system. When you have your preset, make sure under the Resources tab, 'Export all resources in the project' is selected. Otherwise, all the default stuff is good. Personally, I keep all my Godot exports in Exports folder next to my Godot projects, but you put it somewhere accessible. If you have an option, export in Debug Mode. </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_Export.png" alt="(Screenshot: Default export options for Linux.)">
|
||||||
|
|
||||||
|
<p>If you run in Linux, you can get the debug info in your terminal by opening a terminal in the export directory and typing <code>./ChatServer.x86_64</code> or whatever you named your application. </p>
|
||||||
|
|
||||||
|
<p>...Or you can just run both in their editors for now. No big deal. </p>
|
||||||
|
|
||||||
|
<img src="/static/img/ent/Chatbox_ClientConnected.png" alt="(Screenshot: Both applications are running and connecting to each other.)">
|
||||||
|
|
||||||
|
<p>But however you run, if you run the server first and then the client, you will see your connection in the debug output! You have completed an extremely basic networking application. You can see the full project at this stage on my repo: <a href="https://git.socklab.moe/chimchooree/chatserver/src/commit/95dba37181f2cfa37fe03fe495b1c4b520c91101">server</a> and <a href="https://git.socklab.moe/chimchooree/chatclient/src/commit/a70269e9bcde4cdc66115f14c631b0073cdea91e">client</a>.
|
||||||
|
|
||||||
|
<p>Now let's add some chat features. </p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<p>Last updated August 31, 2022 </p>
|
||||||
|
<br>
|
@ -1 +0,0 @@
|
|||||||
reg import vampire.reg
|
|
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 14 KiB |