Not sure if this is the correct section, feel free to move if need be!
Took me a while to get the awesome Godot Game Engine communicating with Crystal through TCP (not crystal’s fault, mostly mine! :P). So I figured I’d share an example script so others can have a head start.
simple_server.cr:
require "socket"
require "json"
MAX_PAYLOAD_SIZE = 5000 # To prevent CPU abuse. Ideally, rate limiting and iptable limits should be implemented as well
# Example of communicating to Godot through TCP using bidirectional JSON
# Not to be used in production (any code by me)
class Client
property user_id = 0
property socket : TCPSocket
def initialize(@socket = socket)
end
def send(msg, cmd = "SERV")
msg_buffer = {cmd: cmd, message: msg}
new_message = msg_buffer.to_json
socket.write_bytes(new_message.bytesize)
socket.send new_message
end
end
class GameServer
property clients = Array(Client).new
def message_handler(msg, client)
cmd = msg["cmd"]
case cmd
when "PING"
# PONG
client.send msg["message"], "PONG"
end
end
# Each connection gets their own fiber
def handle_connection(socket)
begin
puts "New connection: #{socket}"
client = Client.new(socket)
clients << client
loop do
payload_size = socket.read_bytes(UInt32)
raise "Payload Size is too large or invalid. #{payload_size}" if payload_size > MAX_PAYLOAD_SIZE
raw_message = socket.read_string(payload_size)
# JSON.parse will raise JSON::ParseException error
# This is really cool because any JSON data (keys) that don't match the server's, will raise an exception! Then you can disconnect that user for sending naughty stuff, etc
msg = JSON.parse(raw_message)
message_handler(msg, client)
end
rescue e #
# Stream Error (user disconnected)
clients.delete client
puts "#{socket} disconnected. Connected Users : #{clients.size}"
end
end
def initialize(ip, port)
server = TCPServer.new(ip, port, 1000, true, true)
server.tcp_nodelay = true # Very important for latency
puts "GameServer started #{ip}:#{port}"
while socket = server.accept?
spawn handle_connection(socket)
end
end
end
GameServer.new("0.0.0.0", 9300)
simple_socket.gd
extends Node
var is_really_connected = false
var socket = StreamPeerTCP.new()
var TCPTimer = Timer.new()
var MSG_BUFFER = {}
var RTT = 0 # Round Trip Time
func _ready():
socket.connect_to_host("127.0.0.1", 9300)
TCPTimer.wait_time = 0.5 # Ping Interval
TCPTimer.connect("timeout", self, "TCPTimeout")
TCPTimer.start()
add_child(TCPTimer)
func _process(delta):
if socket.get_status() == 2 and !is_really_connected:
socket.set_no_delay(true) # Important for gameservers
print("Connected to server!")
is_really_connected = true
if is_really_connected:
var bytes = socket.get_available_bytes()
if bytes > 0:
# Notice in crystal, we use u32 as well
var message = socket.get_utf8_string(socket.get_u32())
message_handler(message)
func message_handler(msg):
var json = JSON.parse(msg).result
var cmd = json.cmd
var new_message = json.message
if cmd == "PONG":
RTT = OS.get_ticks_msec() - new_message
print("RTT: %sms" % RTT)
print("Message Received: %s" % json)
func TCPTimeout():
if is_really_connected:
send_to_server(OS.get_ticks_msec())
func send_to_server(msg, cmd = "PING"):
MSG_BUFFER.cmd = cmd
MSG_BUFFER.message = msg
# This looks like magic, but Godot silently sends the length of the msg in bytes ^^ nice!
# (in crystal's server code, we send bytes before the message))
socket.put_utf8_string(to_json(MSG_BUFFER))
Steps:
- Start Server:
crystal simple_server.cr
- Attach the simple_socket.gd script to any node in your Godot scene
- Preview Current Scene (F6) and check the consoles
To easily drag and drop onto any linux VPS, just do
crystal build simple_server.cr --static
Enjoy!