Interacting with Web API in Godot
2024-02-01
Hey hi everyone, I hope you're doing good. In this post, we will understand about interacting with web APIs in Godot.
You can also watch the video version of this blog (with few more additions) -
Outline
- What is an API?
- Using a free API in Godot - Trivia API
- Using a paid API in Godot - ChatGPT API
What is an API?
API stands for Application Programming Interface, and as the name suggests it acts as a way for different software programs to communicate with each other. So an API allows a software program written in one programming language to interact with a software that's written in another programming language.
Let's take an example of a simple game. The game is an endless runner where the highscore of the player is saved in their profile (user account). Now, the player wants to compare his score from the others, so he goes to the leaderboard.
- Player opens up the leaderboard menu.
- The game (client) sends HTTP Request to the server, to retrieve the list of players.
- The server receives the request, processes it, and sends back HTTP Response to the game.
- Game receives the data in HTTP Response, and uses it to show the list of players ranked based on their highscore.
That's the flow of sending and receiving data, which happens via the HTTP Protocol.
For this communication to be of any use, the data communicated between the client and the server should be compatible with the programming language in which the applications of client and server are written in. As such, the data sent back and forth needs to be in format that all languages support. The most widely used data format for this purpose is JSON (JavaScript Object Notation).
Now with all that said, you must be wondering where does an API comes into play?
So, the API in the above example of game leaderboard is defined in the server. Whenever, a server receives a request on one of its API… the function defined for that API is executed… which upon finishing returns a response to the client.
HTTP Request
The request sent by the client to the server over the HTTP Protocol is the HTTP Request. In this explanation, I talk about REST API specifically.
These are the contents of HTTP Request -
Let's quickly go through these -
- URL
- Specifies the address through which we can contact the API.
- Methods
- Defines the type of operation our request wants server to do.
- Headers
- Acts as metadata for the request, so server can know the type of data it gets.
- Request Body
- Used mainly with POST/PUT methods to send data that requires more information and has to be structured.
HTTP Response
The response sent by the server to the client over the HTTP Protocol is the HTTP Request. In this explanation, I talk about REST API specifically.
These are the contents of HTTP Response -
It's mostly the same with just one notable difference -
- Doesn't use methods like GET, POST, etc. as these are designed for HTTP Request to tell what action the server should perform.
- Instead the HTTP Response gives status codes that gives the result of the request after being processed by the server.
Using a free API in Godot - Trivia API
You can find the trivia API at - https://opentdb.com/api_config.php
Step 1 - Generate URL for accessing the API
- On its website, click the button GENERATE API URL. The URL will show up at top of the page.
- Go to the URL.
- You will see the webpage shows a bunch of text, that looks like a key-value pair, similar to GDScript's dictionary. This dictionary looking text is a JSON object.
Step 2 - Make the same request in Godot
Create a new scene of type HTTPRequest
node then save it and add a script to it. Add the following lines -
extends HTTPRequest
class_name TriviaAPI
var url: String = "https://opentdb.com/api.php?amount=10"
func _ready() -> void:
request_completed.connect(_on_request_completed)
func make_request() -> void:
request(url)
func _on_request_completed(_result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
pass
Here, the HTTP request is initiated through request(url)
statement in make_request()
function.
Whereas, _on_request_completed
function is connected to request_completed
signal of HTTPRequest node. So this signal will be emitted when we get a response.
Let's define the function _on_request_completed
-
func _on_request_completed(_result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var data: String = body.get_string_from_utf8() // Gets the JSON data from request body
var json: Dictionary = JSON.parse_string(data) // Parses the data to get a dictionary
print(json)
We first get the data as a string from the body argument. Then, parse it to a dictionary so we can easily access the contents of the JSON.
Finally, call the function make_request()
in _ready()
-
func _ready() -> void:
request_completed.connect(_on_request_completed)
make_request()
Run the scene, wait for a moment, and you'll see the response outputted in the console.
Using a paid API in Godot - ChatGPT API
Now let's use OpenAI's text generation API, which basically uses ChatGPT 3.5 as of writing.
Before starting, it's important to note that to use their API you will need to buy credits. For personal use I find them to be quite affordable. But if you don't wanna buy them, you can still go through this section nevertheless, and understand how to use JSON in request body.
So let's start.
You can go to their docs at - https://platform.openai.com/docs/guides/text-generation
Their docs do a pretty good job in explaining how to use their API. In fact, when you will be using any type of third party APIs for your project, then you pretty much always need to go through that website's documentation to understand how to use their APIs.
Step 1 - Setup the scene
Create a new scene and name it TextgenAPI, then add script to it. Open it up and add the following boilerplate -
extends HTTPRequest
class_name TextgenAPI
var url: String = "https://api.openai.com/v1/chat/completions"
func _ready() -> void:
request_completed.connect(_on_request_completed)
func make_request() -> void:
pass
func _on_request_completed(_result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
pass
Step 2 - Define headers
Let's start by defining the request. We set the headers for the request in make_request()
function.
func make_request() -> void:
var headers: PackedStringArray = [
"Content-Type: application/json",
"Authorization: Bearer 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"
]
The headers gives more context to the server about the request we are sending.
Content-Type: application/json
tells to the server that request body contains data in JSON format.Authorization: Bearer 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
basically let the server identify whether the person making the request is authorized or not.
The long string of characters is the key which identifies the user (developer) who is using OpenAI's API. This will let them charge the user an amount of credits on every request sent. So ensure this key is kept safe.
Now putting the key directly in code like how I have done here is not advised at all. What we usually do is create a env file where we store the secret key. Then, wherever we have to use the key we simply access the key via the env file. This is how it's done in web development at least.
For Godot, I am not fully sure what's the right way.
But I will be replicating the env
method in Godot in a sort of hacky way. So I create a env.gd
script in Godot, and add the following -
extends Node
class_name ENV
static var OPEN_AI_API_KEY: String = "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"
Then, in text_gen_api
script, I update the headers statement -
func make_request() -> void:
var headers: PackedStringArray = [
"Content-Type: application/json",
"Authorization: Bearer " + ENV.OPEN_AI_API_KEY
]
Step 3 - Define the Request Body
I now define the request body as -
func make_request() -> void:
var headers: PackedStringArray = [
"Content-Type: application/json",
"Authorization: Bearer " + ENV.OPEN_AI_API_KEY
]
var request_payload: String = JSON.stringify({
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "How are you doing?"
}
]
})
var error = request(url, headers, HTTPClient.METHOD_POST, request_payload)
if error != OK:
push_error("Couldn't make the request.")
Here, I made a dictionary which I pass into class JSON's stringify()
method. This parses the dictionary and return JSON in plain string.
In dictionary, I have structured the format as it was mentioned in OpenAI's docs.
Finally, I pass in the values to the request
method. Here, headers
and request_payload
are already described. For the argument HTTPClient.METHOD_POST
it tells the method of the HTTP request. So I am using POST
method as mentioned in the docs.
That's it. Running the scene, we will get the output printed on screen.
Step 4 - Get the reply message of ChatGPT
We can now access the main message we got from text generation API -
func _on_request_completed(_result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var data: String = body.get_string_from_utf8() // Gets the JSON data from request body
var json: Dictionary = JSON.parse_string(data) // Parses the data to get a dictionary
print(json["choices][0]["message"]["content"])
Now if we run the scene, we get ChatGPT's reply message printed out.
OPTIONAL - Get JSON in ChatGPT's Response Message
When we talk with ChatGPT on their website, we get response in plain text. But if we tell it to give us a response in JSON, then it will give us a response in the JSON.
We can request the same with text generation API, and it will give us the message in JSON. This can prove really helpful in getting response in a particular format so we can extract data out of it and do cool stuff with that information.
So I'm gonna ask GPT to give me a quiz on any topic of my choice.
The thing that's gonna be different is that ChatGPT/text generation API will gonna give me the message in JSON.
So if we want ChatGPT to always give us a reply message in JSON, we need to specify a few properties as described in their JSON mode docs section. The updated code will look like this -
func make_request() -> void:
var headers: PackedStringArray = [
"Content-Type: application/json",
"Authorization: Bearer " + ENV.OPEN_AI_API_KEY
]
var request_payload: String = JSON.stringify({
"model": "gpt-3.5-turbo-1106",
"response_format": {
"type": "json_object"
},
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "How are you doing?"
}
]
})
var error = request(url, headers, HTTPClient.METHOD_POST, request_payload)
if error != OK:
push_error("Couldn't make the request.")
Here, I updated model
property, and added a new property response_format
in the request body.
Now to tell GPT to give us quiz questions with a specified format, I write that out in content
property with the role
of system.
func make_request(question: String) -> void:
var headers: PackedStringArray = [
"Content-Type: application/json",
"Authorization: Bearer " + ENV.OPEN_AI_API_KEY
]
var request_payload: String = JSON.stringify({
"model": "gpt-3.5-turbo-1106",
"response_format": {
"type": "json_object"
},
"messages": [
{
"role": "system",
"content": "You are the quiz master, who always gives response in JSON format. Give me 10 questions related to the question I ask. It should have 4 options, with one correct among them. This is the format: {\"message\":\"Success\",\"questions\":[{\"question\":\"String\",\"correct_answer\":\"String\",\"options\":[\"String\",\"String\",\"String\",\"String\"]}]} and if the question doesn't make sense then the format should be: {\"message\":\"Invalid question\",\"questions\":[]}"
},
{
"role": "user",
"content": question
}
]
})
var error = request(url, headers, HTTPClient.METHOD_POST, request_payload)
if error != OK:
push_error("Couldn't make the request.")
Here, I have escaped the JSON example in single line. The JSON example I defined is not a valid JSON, but ChatGPT is smart enough to interpret it and give us the correct response.
You'll also notice that I added question
argument to the function, which I use in the content
property with role
of user. So when calling this function we can pass any string, on which we get a quiz of 10 multiple choice questions.
Let's call the function, and ask GPT to give us a quiz on solar system!
func _ready() -> void:
request_completed.connect(_on_request_completed)
make_request("Ask me question on Solar System")
Run the scene, wait for a few seconds (it may take some time here), and you'll see that this time in the response, the message that GPT provide will be in JSON format.
Conclusion
That's it. I hope this post gives you a basic idea on interacting with web APIs in Godot. To see the full process in action you can watch my video on it, and to check out the project itself you can download it from over here.
Resources
License
The following blog is licensed under CC BY 4.0 Arpit Srivastava.