Building a Godot Engine Tool Script for Interacting with OpenAI 

Hey Guys,  

This is finepointcgi, and today we’re going to build a integration with Godot that interacts with OpenAI. This integration will not only enhance your coding experience but also provide you with a powerful tool that can help you in various ways. 

This tool will allow us to: 

  1. Automatically comment our code – By simply clicking a ‘Summarize’ button, we can have our code commented automatically. This feature can save us a lot of time and effort, especially when we’re dealing with complex code structures. 
  1. Perform actions on code – Imagine being able to instruct your editor to build or refactor something, and it does it for you. That’s exactly what we’re aiming for. This feature will allow us to perform actions on our code directly from our editor. 
  1. Provide help with code – If you ever find yourself stuck with a piece of code, this tool will come to your rescue. By selecting the code and clicking the ‘Help’ button, you’ll get some helpful text that can guide you to solve your problem. 
  1. Chat with the OpenAI API – This feature will enable us to have a conversation with the OpenAI API. We can ask questions and get answers directly within the Godot editor. This can be a great way to troubleshoot issues or get suggestions for code improvements. 

Now, let’s get started with the process. 

Getting our API Key 

The first thing we need to do is get our API key from OpenAI. If you’ve never used OpenAI’s API before, you’ll need to create an account. Head over to platform.openai.com, create an account, and sign in.  

Once you’re in, navigate to your personal section and click on ‘View API Keys’. You’ll see a list of keys.  

We’ll need to create a new secret key for this project. Click on ‘Create’, put in a name, and copy the generated key. 

Once you have your API key ready, it’s time to jump into Godot and start coding our add-on. 

Getting into Godot

The first step is to set up the add-on in Godot. Navigate to ‘Project’ > ‘Project Settings’ > ‘Plugins’ and create a new plugin.

Name it ‘Godot GPT Integration’ and set the subfolder as ‘res://GPT_Integration’. For the author, you can put ‘FinePointCGI’. Set the version to ‘1.0’ and choose ‘GDScript’ as the language.

You could use C#, but keep in mind that if users don’t have the Mono version of Godot, they won’t be able to use your add-on.

Once you’ve filled in all the details, click ‘Create’. 

This will create an editor plugin. Editor plugins are scripts that run during editor time and allow you to extend the editor itself. 

Creating The Scene

Next, we need to create a user interface for our plugin. This will include a chat box, a text edit field, and several buttons. Here’s how to do it:

  1. Right-click on the 2D scene and add a child node. Choose ‘TextEdit’ and set it to ‘Full Rect’. This will be our chat box. Adjust its size to your liking.
  2. Add another child node, this time a ‘Button’. Snap it to the bottom right of the scene and adjust its size. Set its text to ‘Chat’.
  3. Add a ‘TextEdit’ node and snap it to the bottom right of the scene. Adjust its size and position it above the ‘Chat’ button.
  4. Add an ‘HBoxContainer’ to the scene and snap it to the full bottom of the screen. Adjust its size.
  5. Right-click on the ‘HBoxContainer’ and add a child node. Choose ‘Button’ and duplicate it three times. Name the buttons ‘Summary’, ‘Action’, and ‘Help’. Adjust their text accordingly.
  6. Select all the buttons, go to ‘Layout’ > ‘Content’ > ‘Container Sizing’ and expand to fit the width of the ‘HBoxContainer’.
  7. Adjust the size of the chat box and the text edit field to match the size of the buttons.
  8. Optionally, you can add an ‘HSeparator’ to the scene for aesthetic purposes.

This is what it should look like

Once you’ve set up the user interface, save the scene as ‘chat.tscn’ and change the name of the control node to ‘Chat’. This is important as it sets the tab’s name in the Godot editor.

Now that we have our environment set up, we can start coding the plugin. First, we need to create a variable for our object. We’ll call it ‘chat_doc’. Then, we’ll instantiate our object and add it to the dock. We’ll choose the upper right dock for this tutorial, but you can choose any dock you prefer.

Once you’ve done that, you can disable and enable your plugin in the project settings. You should see a new tab named ‘Chat’ in the Godot editor. This is your chat window.

Setting Up the Script 

First, let’s set up our script. We’ll start by extending the `EditorPlugin` class and declaring some variables and constants. Here’s the initial setup: 

@tool 
extends EditorPlugin 
const MySettings:StringName = "res://addons/GPTIntegration/settings.json" 

enum modes { 
 Action,  
 Summarise, 
 Chat, 
 Help 
} 

var api_key = "" ## put the API key here
var max_tokens = 1024 # How many tokens can you use to generate your response
var temperature = 0.5 # how "crazy" you want it to be
var url = "https://api.openai.com/v1/completions" # the open api url
var headers = ["Content-Type: application/json", "Authorization: Bearer " + api_key] 
var engine = "text-davinachi-003" # What engine you want to use
var chat_dock 
var http_request :HTTPRequest 
var current_mode 
var cursor_pos 
var code_editor 
var settings_menu

In this setup, we’re declaring a constant `MySettings` that points to a JSON file where we’ll store our settings. We’re also declaring an enum `modes` that represents the different modes our script can be in.

The `api_key`, `max_tokens`, `temperature`, `url`, `headers`, and `engine` variables are used for interacting with the OpenAI API. The `chat_dock`, `http_request`, `current_mode`, `cursor_pos`, `code_editor`, and `settings_menu` variables are used for interacting with the Godot editor. 

Entering the Tree 

When our script enters the tree, we want to instantiate our chat dock, add it to the left upper dock, and connect the buttons in the chat dock to their respective functions. We also want to add a tool menu item that will show our settings when clicked. Here’s how to do it: 

func _enter_tree(): 
 print('_enter_tree') 
 chat_dock = preload("res://addons/GPTIntegration/Chat.tscn").instantiate() 
 add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, chat_dock) 
 chat_dock.get_node("Button").connect("pressed", _on_chat_button_down) 
 chat_dock.get_node("HBoxContainer/Action").connect("pressed", _on_action_button_down) 
 chat_dock.get_node("HBoxContainer/Help").connect("pressed", _on_help_button_down) 
 chat_dock.get_node("HBoxContainer/Summary").connect("pressed", _on_summary_button_down) 
 add_tool_menu_item("GPT Chat", on_show_settings) 
 load_settings() 
 pass

In this code, we’re preloading the chat dock scene, instantiating it, and adding it to the left upper dock. We’re then getting the ‘Button’, ‘Action’, ‘Help’, and ‘Summary’ nodes from the chat dock and connecting their ‘pressed’ signals to the `_on_chat_button_down`, `_on_action_button_down`, `_on_help_button_down`, and `_on_summary_button_down` functions, respectively. We’re also adding a tool menu item labeled “GPT Chat” that calls the `on_show_settings` function when clicked. Finally, we’re calling the `load_settings` function to load our settings from the JSON file. 

Exiting the Tree 

When our script exits the tree, we want to remove the chat dock from the docks, 

queue it for deletion, remove the tool menu item, and save our settings. Here’s how to do it: 

func _exit_tree(): 
 remove_control_from_docks(chat_dock) 
 chat_dock.queue_free() 
 remove_tool_menu_item("GPT Chat") 
 save_settings() 
 pass

In this code, we’re removing the chat dock from the docks and queuing it for deletion. We’re also removing the “GPT Chat” tool menu item. Finally, we’re calling the `save_settings` function to save our settings to the JSON file. 

Handling Button Presses 

Next, let’s handle the button presses. When the ‘Chat’ button is pressed, we want to send a chat request to OpenAI with the text in the ‘TextEdit’ node of the chat dock. Here’s how to do it: 

func _on_chat_button_down(): 
 if(chat_dock.get_node("TextEdit").text != ""): 
  current_mode = modes.Chat 
  var prompt = chat_dock.get_node("TextEdit").text 
  chat_dock.get_node("TextEdit").text = "" 
  add_to_chat("Me: " + prompt) 
  call_GPT(prompt)

In this code, we’re checking if the text in the ‘TextEdit’ node of the chat dock is not empty. If it’s not, we’re setting the current mode to ‘Chat’, getting the text from the ‘TextEdit’ node, clearing the ‘TextEdit’ node, adding the text to the chat with the prefix “Me: “, and calling the `call_GPT` function with the text. 

When the ‘Action’ button is pressed, we want to send a request to OpenAI with a prompt that tells it to perform an action on the currently selected code. Here’s how to do it:

func _on_action_button_down(): 
 current_mode = modes.Action 
 call_GPT("Code this for Godot " + get_selected_code())

In this code, we’re setting the current mode to ‘Action’ and calling the `call_GPT` function with a prompt that tells OpenAI to perform an action on the currently selected code. 

When the ‘Help’ button is pressed, we want to send a request to OpenAI with a prompt that asks for help with the currently selected code. Here’s how to do it: 

func _on_help_button_down(): 
 current_mode = modes.Help 
 var code = get_selected_code() 
 chat_dock.get_node("TextEdit").text = "" 
 add_to_chat("Me: " + "What is wrong with this GDScript code? " + code) 
 call_GPT("What is wrong with this GDScript code? " + code)

In this code, we’re setting the current mode to ‘Help’, getting the currently selected code, clearing the ‘TextEdit’ node of the chat dock, adding a question to the chat with the prefix “Me: “, and calling the `call_GPT` function with a prompt that asks OpenAI for help with the code. 

When the ‘Summary’ button is pressed, we want to send a request to OpenAI with a prompt that tells it to summarize the currently selected code. Here’s how to do it: 

func _on_summary_button_down(): 
 current_mode = modes.Summarise 
 call_GPT("Summarize this GDScript Code " + get_selected_code())

In this code, we’re setting the current mode to ‘Summarise’ and calling the `call_GPT` function with a prompt that tells OpenAI to summarize the currently selected code. 

Handling the Response 

Next, let’s handle the response from OpenAI. We’ll do this in the `_on 

_request_completed` function. Depending on the current mode, we’ll either add the response to the chat, insert it into the code editor as a summarized version of the code, or insert it into the code editor as an action to be performed on the code. Here’s how to do it: 

func _on_request_completed(result, responseCode, headers, body): 
 print(result, responseCode, headers, body) 
 var json = JSON.new() 
 var parse_result = json.parse(body.get_string_from_utf8()) 
 if parse_result: 
  printerr(parse_result) 
  return 
 var response = json.get_data() 
 if response is Dictionary: 
  print("Response", response) 
  if response.has("error"): 
   print("Error", response['error']) 
   return 
 else: 
  print("Response is not a Dictionary", headers) 
  return 
 var newStr = response.choices[0].text 
 if current_mode == modes.Chat: 
  add_to_chat("GPT: " + newStr) 
 elif current_mode == modes.Summarise: 
  var str = response.choices[0].text.replace("\n" , "") 
  newStr = "# " 
  var lineLength = 50 
  var currentLineLength = 0 
  for i in range(str.length()): 
   if currentLineLength >= lineLength and str[i] == " ": 
    newStr += "\n# " 
    currentLineLength = 0 
   else: 
    newStr += str[i] 
    currentLineLength += 1 
  code_editor.insert_line_at(cursor_pos, newStr) 
 elif current_mode == modes.Action: 
  code_editor.insert_line_at(cursor_pos, newStr) 
 elif current_mode == modes.Help: 
  add_to_chat("GPT: " + response.choices[0].text) 
 pass

In this code, we’re parsing the response from OpenAI and checking if it’s a dictionary. If it is, we’re extracting the text from the response. If the current mode is ‘Chat’, we’re adding the text to the chat with the prefix “GPT: “. If the current mode is ‘Summarise’, we’re looping through the text and inserting it into the code editor as a summarized version of the code with each line having a maximum length of 50 characters and each line starting with a “#”. If the current mode is ‘Action’, we’re inserting the text into the code editor. If the current mode is ‘Help’, we’re adding the text to the chat with the prefix “GPT: “. 

Saving and Loading Settings 

Finally, let’s handle saving and loading settings. We’ll do this in the `save_settings` and `load_settings` functions. In the `save_settings` function, we’ll create a JSON file containing the values of the `api_key`, `max_tokens`, `temperature`, and `engine` variables, and store the file in the “res://addons/GPTIntegration/” directory. In the `load_settings` function, we’ll open the JSON file, parse the data from it, and assign the values to the `api_key`, `max_tokens`, `temperature`, and `engine` variables. Here’s how to do it: 

func save_settings(): 
 var data ={ 
  "api_key":api_key, 
  "max_tokens": max_tokens, 
  "temperature": temperature, 
  "engine": engine 
 } 
 print(data) 
 var jsonStr = JSON.stringify(data) 
 var file = FileAccess.open(MySettings, FileAccess.WRITE) 
 file.store_string(jsonStr) 
 file.close() 

In the `save_settings` function, we’re creating a dictionary with the values of the `api_key`, `max_tokens`, `temperature`, and `engine` variables, converting the dictionary to a JSON string, opening the settings file in write mode, storing the JSON string in the file, and closing the file. 

func load_settings(): 
 if not FileAccess.file_exists(MySettings): 
  save_settings() 
 var file = FileAccess.open(MySettings, FileAccess.READ) 
 if not file: 
printerr("Unable to create", MySettings, error_string(ERR_CANT_CREATE)) 
  print_stack() 
  return 
 var jsonStr = file.get_as_text() 
 file.close() 
 var data = JSON.parse_string(jsonStr) 
 print(data) 
 api_key = data["api_key"] 
 max_tokens = int(data["max_tokens"]) 
 temperature = float(data["temperature"]) 
 engine = data["engine"]

In the `load_settings` function, we’re checking if the settings file exists. If it doesn’t, we’re calling the `save_settings` function to create it. We’re then opening the settings file in read mode, getting the text from the file, closing the file, parsing the text into a dictionary, and assigning the values from the dictionary to the `api_key`, `max_tokens`, `temperature`, and `engine` variables. 

@tool
extends EditorPlugin

const MySettings:StringName = "res://addons/GPTIntegration/settings.json"

enum modes {
	Action, 
	Summarise,
	Chat,
	Help
}

var api_key = ""
var max_tokens = 1024
var temperature = 0.5
var url = "https://api.openai.com/v1/completions"
var headers = ["Content-Type: application/json", "Authorization: Bearer " + api_key]
var engine = "text-davinachi-003"
var chat_dock
var http_request :HTTPRequest
var current_mode
var cursor_pos
var code_editor
var settings_menu

func _enter_tree():
	printt('_enter_tree')
	chat_dock = preload("res://addons/GPTIntegration/Chat.tscn").instantiate()
	add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, chat_dock)
	
	chat_dock.get_node("Button").connect("pressed", _on_chat_button_down)
	chat_dock.get_node("HBoxContainer/Action").connect("pressed", _on_action_button_down)
	chat_dock.get_node("HBoxContainer/Help").connect("pressed", _on_help_button_down)
	chat_dock.get_node("HBoxContainer/Summary").connect("pressed", _on_summary_button_down)
	# Initialization of the plugin goes here.
	add_tool_menu_item("GPT Chat", on_show_settings)
	load_settings()
	pass

func on_show_settings():
	settings_menu = preload("res://addons/GPTIntegration/SettingsWindow.tscn").instantiate()
	settings_menu.get_node("Control/Button").connect("pressed", on_settings_button_down)
	set_settings(api_key, int(max_tokens), float(temperature), engine)
	add_child(settings_menu)
	settings_menu.connect("close_requested", settings_menu_close)
	settings_menu.popup()
	
func on_settings_button_down():
	api_key = settings_menu.get_node("HBoxContainer/VBoxContainer2/APIKey").text
	
	max_tokens = int(settings_menu.get_node("HBoxContainer/VBoxContainer2/MaxTokens").text)
	temperature = float(settings_menu.get_node("HBoxContainer/VBoxContainer2/Temperature").text)
	var index = settings_menu.get_node("HBoxContainer/VBoxContainer2/OptionButton").selected
	
	if engine == "text-davinchi-003":
		index = 0
	elif index == 1:
		engine = "code-davinci-002"
	elif index == 2:
		engine = "text-curie-001"
	elif index == 3: 
		engine = "text-babbage-001"
	elif index == 4:
		engine = "text-ada-001"
	elif index == 5:
		engine = "code-cushman-002"
		
	settings_menu_close()
	pass

func settings_menu_close():
	settings_menu.queue_free()
	pass

# This GDScript code sets the settings in a settings
# menu. It sets the API key, the maximum number of tokens,
# the temperature, and the engine. The engine is set
# by selecting the corresponding ID from a list of options.
func set_settings(api_key, maxtokens, temp, engine):
	settings_menu.get_node("HBoxContainer/VBoxContainer2/APIKey").text = api_key
	settings_menu.get_node("HBoxContainer/VBoxContainer2/MaxTokens").text = str(maxtokens)
	settings_menu.get_node("HBoxContainer/VBoxContainer2/Temperature").text = str(temp)
	var id = 0
	
	if engine == "text-davinchi-003":
		id = 0
	elif  engine == "code-davinci-002":
		id = 1
	elif engine == "text-curie-001":
		id = 2
	elif engine == "text-babbage-001":
		id = 3
	elif engine == "text-ada-001":
		id = 4
	elif engine == "code-cushman-002":
		id = 5
		
	settings_menu.get_node("HBoxContainer/VBoxContainer2/OptionButton").select(id)

func _exit_tree():
	# Clean-up of the plugin goes here.
	remove_control_from_docks(chat_dock)
	chat_dock.queue_free()
	remove_tool_menu_item("GPT Chat")
	save_settings()
	pass

func _ready():
	http_request = HTTPRequest.new()
	add_child(http_request)
	http_request.connect("request_completed", _on_request_completed)

func _on_chat_button_down():
	if(chat_dock.get_node("TextEdit").text != ""):
		current_mode = modes.Chat
		var prompt = chat_dock.get_node("TextEdit").text
		chat_dock.get_node("TextEdit").text = ""
		add_to_chat("Me: " + prompt)
		call_GPT(prompt)
		
func call_GPT(prompt):
	var body = JSON.new().stringify({
		"prompt": prompt,
		"temperature": temperature,
		"max_tokens": max_tokens,
		"model": "text-davinci-003"
	})
	var error = http_request.request(url, ["Content-Type: application/json", "Authorization: Bearer " + api_key], HTTPClient.METHOD_POST, body)
	
	if error != OK:
		push_error("Something Went Wrong!")

func _on_action_button_down():
	current_mode = modes.Action
	call_GPT("Code this for Godot " + get_selected_code())
	
func _on_help_button_down():
	current_mode = modes.Help
	var code = get_selected_code()
	chat_dock.get_node("TextEdit").text = ""
	add_to_chat("Me: " + "What is wrong with this GDScript code? " + code)
	call_GPT("What is wrong with this GDScript code? " + code)
	
func _on_summary_button_down():
	current_mode = modes.Summarise
	call_GPT("Summarize this GDScript Code " + get_selected_code())
	


# This GDScript code is used to handle the response from
# a request and either add it to a chat or summarise
# it. If the mode is set to Chat, it will add the response
# to the chat with the prefix "GPT". If the mode is set
# to Summarise, it will loop through the response and
# insert it into the code editor as a summarised version
# with each line having a maximum length of 50 characters
# and each line starting with a "#".
func _on_request_completed(result, responseCode, headers, body):
	printt(result, responseCode, headers, body)
	var json = JSON.new()
	var parse_result = json.parse(body.get_string_from_utf8())
	if parse_result:
		printerr(parse_result)
		return
	var response = json.get_data()
	if response is Dictionary:
		printt("Response", response)
		if response.has("error"):
			printt("Error", response['error'])
			return
	else:
		printt("Response is not a Dictionary", headers)
		return
	
	var newStr = response.choices[0].text
	if current_mode == modes.Chat:
		add_to_chat("GPT: " + newStr)
	elif current_mode == modes.Summarise:
		var str = response.choices[0].text.replace("\n" , "")
		newStr = "# "
		var lineLength = 50
		var currentLineLength = 0
		for i in range(str.length()):
			if currentLineLength >= lineLength and str[i] == " ":
				newStr += "\n# "
				currentLineLength = 0
			else:
				newStr += str[i]
				currentLineLength += 1
		code_editor.insert_line_at(cursor_pos, newStr)
	elif current_mode == modes.Action:
		code_editor.insert_line_at(cursor_pos, newStr)
	elif current_mode == modes.Help:
		add_to_chat("GPT: " + response.choices[0].text)
	pass
	
func get_selected_code():
	var currentScriptEditor = get_editor_interface().get_script_editor().get_current_editor()
	
	code_editor = currentScriptEditor.get_base_editor()
	
	if current_mode == modes.Summarise:
		cursor_pos = code_editor.get_selection_from_line()
	elif current_mode == modes.Action:
		cursor_pos = code_editor.get_selection_to_line()
	return code_editor.get_selected_text()

func add_to_chat(text):
	var chat_bubble = preload("res://addons/GPTIntegration/ChatBubble.tscn").instantiate()
	chat_bubble.get_node("RichTextLabel").text = "\n" + text + "\n"
	chat_dock.get_node("ScrollContainer/VBoxContainer").add_child(chat_bubble)
	#chat_dock.get_node("RichTextLabel").text += "\n" + text + "\n"

# This GDScript code creates a JSON file containing the
# values of the variables "api_key", "max_tokens", "temperature",
# and "engine", and stores the file in the "res://addons/GPTIntegration/"
# directory.
func save_settings():
	
	var data ={
		"api_key":api_key,
		"max_tokens": max_tokens,
		"temperature": temperature,
		"engine": engine
	}
	print(data)
	var jsonStr = JSON.stringify(data)
	var file = FileAccess.open(MySettings, FileAccess.WRITE)
	
	file.store_string(jsonStr)
	file.close()

# This GDScript code opens a JSON file, parses the data
# from it, and assigns the values to variables. The variables
# are api_key, max_tokens, temperature, and engine.
func load_settings():
	if not FileAccess.file_exists(MySettings):
		save_settings()

	var file = FileAccess.open(MySettings, FileAccess.READ)
	if not file:
		printerr("Unable to create", MySettings, error_string(ERR_CANT_CREATE))
		print_stack()
		return

	var jsonStr = file.get_as_text()
	file.close()
	var data = JSON.parse_string(jsonStr)
	print(data)
	api_key = data["api_key"]
	max_tokens = int(data["max_tokens"])
	temperature = float(data["temperature"])
	engine = data["engine"]

Conclusion

And that’s it! We’ve built a Godot Engine tool script that interacts with OpenAI. With this script, we can chat with OpenAI’s GPT-3.5 model, get help with our code, perform actions on our code, and even summarize our code. I hope you found this tutorial helpful.

Companion Video

Leave a Reply

Your email address will not be published. Required fields are marked *