I want to get web3 gaming out of the browser and into a format many gamers expect: standalone apps downloaded from a marketplace and played locally. Rather than webwallets, embedded keystores on the game's backend would handle players' connection to the blockchain, and instead of game servers, decentralized storage platforms and databases would allow players to share a game world they create together.
I built the Island of Space & Time with these ideas in mind, using a SxT table to store images that players have generated using artificial intelligence. Controlling the rules of this interaction is a smart contract, which restricts the kinds of queries that can be made to the AI image generator, and which holds various pieces of information that affect the off-chain game world. Player keys are generated by the game and used to sign transactions when certain in-game events are triggered.
Go to the heart of the Island and summon creatures. Encounter them where other players have sent them!
Create images using Dall-E and Chainlink VRF, with parameters bound by a smart contract. With SxT, images are shared with all players, without needing to connect to a centralized server. Player-created creatures live on-chain. Gameplay is off-chain.
- WASD/Arrow keys to move
- Mouse to look
- E to interact
- Spacebar to jump
- Shift to run
- C / ESC to capture/uncapture mouse
Download the game client for your system here. Currently, only Mac ARM is supported (Intel Mac may also work, but has not been tested).
If the SxT relay is operational, you should need only the game client to enter the Island. However, if you want to summon creatures, you will need to provide your own OpenAI API key on the login screen. Please note that you will incur the usual fees for generating images. If the relay is available, you may use it by leaving the SxT field blank. Otherwise, you will need to provide your own SxT REFRESH token to summon and see creatures.
The game will generate an embedded keystore file for you when you first start playing. Click the "Copy Address" and "Get MATIC" buttons to get gas from the faucet.
How It Works
The game uses an SxT table to store images shared between all players. Creatures are created by first calling Chainlink VRF, which randomly inserts words into an on-chain AI prompt form. Dall-E takes the prompt and returns a base64 string, which is uploaded to a SxT table along with a unique hash value and the creature's ID.
On-chain, the hash is mapped to the creature ID, and the creature's base statistics are set.
When Godot loads creatures into the game, it first checks whether the creature has been properly initialized on-chain before loading its image from the SxT table. Godot also pulls each creature's base stats from the smart contract. A creature's "location" can be set with an on-chain transaction, and Godot will use this setting to determine where creatures are loaded into the game world.
This is made possible using Godot Rust in conjunction with Ethers.rs, which allows the game to create a local keystore and perform transactions. To be truly secure, the game will need to use Chainlink Functions to control how images are created and uploaded.
Image generation works by taking your OpenAI API key, performing a query, receiving a base64 string, and uploading that string to Space and Time.
I spent a great deal of time working to integrate Chainlink Functions into the game. Namely, I ported the secrets encryption functionality of the starter kit into Rust, and built a tool that allows Godot Rust to create SxT biscuits that would be used for secret-passing. Currently, no Chainlink Functions call takes place, and your OpenAI API key is not passed to a DON. Godot instead handles the API call and uploads the image by itself. Currently, Chainlink Functions' HTTP Maximum request length is 2kb, while the base64 strings from OpenAI are approximately 250kb.
I've designed a system mimicking the Functions starter kit's Github Gist system that would instead use SxT tables as a means of passing secrets to the Chainlink DON. You can find the code for this implementation on Github under the "secrets" folder. The scheme would work as follows:
Godot encrypts your OpenAI API key, creates a new SxT biscuit, uses the biscuit to create a private table on SxT, and places the encrypted key there.
Godot then uses the DON public key to encrypt the access biscuit, the SxT access token, the table's randomized name, and the encrypted OpenAI key's decryption key. These values are sent on-chain to the DON, and only the DON can decrypt them.
Once the DON receives the encrypted payload, the DON accesses your OpenAI key, decrypts it, performs the OpenAI request, and then deletes the table. Godot also performs a follow-up sanity check to ensure the table is deleted.
Problems & Solutions
I began with an idea to use Chainlink Functions, AI-generated images, and the excellent Metamask plugin for Godot. I tested OpenAI image generation using the Functions starter kit. Then I started in on Godot, initially writing much of the code for HTML5 export, as I needed to make use of the browser for access to Metamask. I got Chainlink VRF working, made some hexagons in Blender, found a great first person controller, a fantastic level-building tutorial, and a nice skybox.
However, when I saw that biscuit-auth has a Rust implementation, and found the awesome community and documentation of Godot-Rust, I decided to move in that direction. I discovered quickly however that the crates I wanted to use had a difficult time working with Godot's HTML5 export template.
While the Metamask plugin is very user-friendly, I had originally conceived of this project as a standalone app that didn't require a browser, with Metamask just serving as a prebuilt signer for my proof of concept. So recreating the signer in Rust was a welcome challenge.
I first ported biscuit-auth into Godot, and used it to create tables. Then I started work on smart contract implementation.
Ethers.rs is a handy crate, built for managing keys and sending transactions, with an additional crate that makes interacting with contracts very easy. With some help from this tutorial, I was able to create some basic scripts.
I then tried to replicate the functionality of encryptWithSignature(). Some days and many crates later, I believed I had a working prototype of my SxT secret passing idea.
But I then realized that Inline secrets were no longer supported. Coupled with the 2kb HTTP request length limit, I decided for now to suspend Chainlink Functions implementation.
In the back of my mind while building was the question of how to make the game accessible to everyone, while also making sure that only images generated by the smart contract would end up in the game.
Players would share an SxT table responsible for holding all images generated by the summoning contract. Because the image query, image table name, and image table biscuit would all need to be public, it was initially difficult to think of a way to protect the table from people potentially circumventing the smart contract.
At first, I reasoned that the biscuit used to upload images to the table either needed to be a secret belonging to the DON, or it needed to have some kind of permissioning on it that only the DON could use.
Eventually it occurred to me that the DON could instead "sign" each image it creates with a hash, and upload the hash both to the SxT image table and on-chain, making it possible for the game client to check which images were genuinely created by the smart contract. This way, the biscuit could be public, and it wouldn't matter if someone tried to spam the table with incorrect images, because Godot would simply filter them out.
However, this introduces another problem. The hash will need to be the key value on the table. While one of the nodes will successfully load the hash and base64 string onto the table, the other nodes aren't going to know what the hash was, meaning they can't check to see that the job succeeded and therefore they cannot safely report a "success" to the contract. I'm curious if Chainlink Functions, as a new feature, could leverage VRF here (as part of the Functions call, not before the call) to produce a hash/signature that is known to all nodes in the DON.
Alternatively, the problem of a public table could be avoided if the Functions DON exclusively possessed the biscuit and was configured to only use it when queried specifically from the summoning contract.
In addition to Functions, I want to build out the embedded wallet functionality. Many web3 games are built catering toward browsers and web3 wallets, but I feel this is a captive audience. I'd like to see more web3 games get out of the browser, with signers built into the game clients themselves, and the costs of blockchain interaction abstracted away as "subscriptions" easily covered by both credit card and crypto.
It would also be simple to add a means of transferring creatures between addresses (or making them full ERC721s).
And of course — gameplay! For now, the game is quite simple, but I want to build out a 3D adventure game where the player interacts with the many creatures that have been created. They will act as allies and opponents in the game.
While the base AI query is generated by Chainlink VRF, on-chain gameplay will provide the player with items that can be used to affect their query, allowing them to generate unique types of creatures. I would like to give players as much latitude as possible, while still ensuring that the creatures created keep the intended aesthetic of the game.
Players will also be able to affect the off-chain experience of other players via on-chain transactions; for example, sending creatures to a specific area of the game world, as shown in the demo. A player can also augment their creatures with on-chain gameplay, changing that creature's abilities and stats when it appears in their game and in other players' games.
Progression in the game itself will be tracked locally. Off-chain gameplay will have a local save file tracking the player's progress and the status of their creatures, separate from the on-chain interactions.
The game's code also needs a substantial rework. The loading times are much too long, which can probably be fixed by creating separate runtimes for Rust async functions that will not interrupt the main Godot thread.
Long term future: my eventual goal, with the advent of stateless EVM clients, would be the inclusion of an embedded client allowing players to become their own RPC nodes, improving their connection and the overall health of the underlying blockchain the game uses.
In addition to the extremely useful help and docs of Chainlink and Space&Time, I wanted to provide a list of the incredible open source information, tools, and assets I used to create this project, without which this would have been impossible.
Godot Rust https://github.com/godot-rust/gdnative
Rust Ethers https://github.com/gakonst/ethers-rs
Forest tutorial https://www.youtube.com/watch?v=0bgw7crtOcQ
UniversalSky plugin https://github.com/dwlcj/UniversalSky/
Waterways Plugin https://github.com/Arnklit/Waterways
Heightmap Terrain Plugin https://github.com/Zylann/godot_heightmap_plugin
Scatter Tool Plugin https://github.com/Zylann/godot_scatter_plugin
FPS Controller https://www.youtube.com/watch?v=Nn2mi5sI8bM
Metal Shader https://godotshaders.com/shader/simple-3d-metal/
Pink Skybox https://github.com/rpgwhitelock/AllSkyFree_Godot
Ground Textures https://ambientcg.com
Heightmap https://heightmap.skydark.pl/beta/ (Somewhere east of Yosemite)
Godot Metamask https://github.com/nate-trojian/MetamaskAddon