Simple Godot -> Crystal TCP Script

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!

1 Like

Nice! I’ve been interested in game programming for a long time, and always naively winced at the idea of using an engine :sweat_smile:

I found Godot myself some months ago, and it’s really fantastic. I compiled it myself and got the tutorial games working with no issues. I wish I had the time to dive into it more, I certainly will some day…

I remember when I first saw you bringing up that you were using Godot and Crystal serverside, and thinking of how awesome of an idea that is - it would look so good for both Godot and Crystal to have a great indie game published someday that uses them.

Anyways - this is a great start for those like me wanting to get their feet wet using this killer stack.

Might I suggest making an example repo on GitHub to compliment this post that people can simply clone and build? :smiley:

(also - Some links to Godot would be nice in your OP for those who don’t know about it)

1 Like

Thanks @z64. Good suggestions. I added more emphasize on what Godot is and linked to the site. I’m not sure about github i’ll think about it, ah. Btw, thanks for your help in the mysql issue(s) months ago. It is appreciated. You and Brian both!

1 Like

We need GDNative bindings to Crystal so we can write everything in Crystal! :exploding_head:

3 Likes

I don’t even know what Godot is, but that still sounds like an extremely good idea! :exploding_head: