Sanitize all posts for public repo
- Remove classified agent names and internal codenames - Remove Tailscale references - Generalise internal details (machine names, team specifics) - Frame everything around Tinqs Studio as the platform - fal.ai post references the image-generation skill - README updated with Studio positioning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
# Tinqs Blog
|
||||
# Tinqs Studio Blog
|
||||
|
||||
Engineering and game development blog from [Tinqs](https://tinqs.com) --- a 4-person indie studio building Ariki, a survival colony sim set in a Polynesian archipelago.
|
||||
Engineering blog from [Tinqs Studio](https://tinqs.com) --- a game development platform built on a Gitea fork with 3D asset preview, LFS-first workflows, AI agent tools, and creative pipelines for game teams.
|
||||
|
||||
We're building Tinqs Studio while using it to make our own game --- a survival colony sim in Godot 4. These posts cover what we've learned along the way.
|
||||
|
||||
## Posts
|
||||
|
||||
- [How a 4-Person Indie Studio Runs on AI Agents](posts/agentic-workflow.md) (2026-03-06)
|
||||
- [How a Small Game Studio Runs on AI Agents](posts/agentic-workflow.md) (2026-03-06)
|
||||
- [One Binary to Rule Them All: Building a Studio CLI](posts/studio-cli.md) (2026-05-18)
|
||||
- [Why We Forked Gitea and Built Our Own Git Platform](posts/forking-gitea.md) (2026-05-20)
|
||||
- [Why We Forked Gitea and Built Tinqs Studio](posts/forking-gitea.md) (2026-05-20)
|
||||
- [Streaming a 12km Archipelago in Godot 4](posts/godot-optimisation.md) (2026-05-22)
|
||||
- [AI Art at Scale: Using fal.ai Flux for Game Asset Generation](posts/fal-image-generation.md) (2026-05-25)
|
||||
|
||||
## Skills
|
||||
|
||||
Reusable AI agent playbooks from our workflow. Each skill is a markdown file that teaches an AI agent (Cursor, Claude Code, etc.) a specific procedure.
|
||||
Reusable AI agent playbooks. Each skill is a markdown file that teaches an AI agent (Cursor, Claude Code, etc.) a specific workflow. Drop them into your `.cursor/skills/` directory.
|
||||
|
||||
- [Image Generation with fal.ai](skills/image-generation.md) --- Generate game art using fal.ai Flux models with structured prompts
|
||||
- [Image Generation with fal.ai](skills/image-generation.md) --- Generate game art using Flux models with structured prompts
|
||||
- [Concept Art Pipeline](skills/concept-art-pipeline.md) --- End-to-end 2D concept art to 3D model workflow
|
||||
- [Sora 2 Video Generation](skills/sora2-video.md) --- Generate trailer clips and game footage with OpenAI Sora 2
|
||||
- [3D Model Generation with Tripo](skills/tripo-browser-workflow.md) --- Text-to-3D and image-to-3D via Tripo Studio
|
||||
@@ -22,9 +24,7 @@ Reusable AI agent playbooks from our workflow. Each skill is a markdown file tha
|
||||
|
||||
## What are skills?
|
||||
|
||||
Skills are structured markdown files that give AI coding assistants (like Cursor or Claude Code) step-by-step procedures for complex workflows. Instead of explaining the same process every session, you write it once as a skill and the agent follows it.
|
||||
|
||||
Think of them as runbooks for AI agents --- same idea as ops runbooks, but the reader is an LLM, not a human.
|
||||
Skills are structured markdown files that give AI coding assistants step-by-step procedures for complex workflows. Instead of explaining the same process every session, you write it once and the agent follows it. Think of them as runbooks for AI agents.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+55
-52
@@ -1,100 +1,103 @@
|
||||
---
|
||||
title: "How a 4-Person Indie Studio Runs on AI Agents"
|
||||
title: "How a Small Game Studio Runs on AI Agents"
|
||||
slug: agentic-workflow
|
||||
date: "2026-03-06"
|
||||
description: "We gave our AI a name, a soul file, and a seat at the table. Here's how Singularity, Sentinel, and three machines keep Tinqs running with a team of four humans and zero DevOps."
|
||||
og_description: "Soul files, autonomous daemons, and one repo to rule them all."
|
||||
description: "Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale."
|
||||
og_description: "Soul files, skill playbooks, and markdown as the universal API for AI agents in game dev."
|
||||
og_image: "https://www.tinqs.com/blog/img/agentic-workflow-architecture.png"
|
||||
excerpt: "We gave our AI a name, a soul file, and a seat at the table. Here's how Singularity, Sentinel, and three machines keep Tinqs running with a team of four humans and zero DevOps."
|
||||
excerpt: "Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
We gave our AI a name, a soul file, and a seat at the table. This is the story of how Tinqs --- four humans making a survival colony sim --- built an agentic workflow that lets us operate like a studio ten times our size.
|
||||
|
||||

|
||||
We gave our AI agents persistent identities, skill playbooks, and access to our entire knowledge base. This is how a 4-person game studio built an agentic workflow that punches above its weight.
|
||||
|
||||
## The Problem Every Small Studio Knows
|
||||
|
||||
When you're four people building a game, there's no room for a dedicated DevOps person, a full-time PM tool chain, or someone whose job it is to "keep things organized." Everyone wears five hats. Documentation drifts. Issues pile up. The left hand doesn't know what the right hand shipped.
|
||||
When you're four people building a game, there's no room for a dedicated DevOps person, a full-time PM tool chain, or someone whose job it is to "keep things organised." Everyone wears five hats. Documentation drifts. Issues pile up. The left hand doesn't know what the right hand shipped.
|
||||
|
||||
We tried the usual tools --- Notion, Trello, shared Google Docs. They all had the same problem: they're passive. They sit there and wait for a human to update them. In a team of four where the CTO is also the sole developer, that human never has time.
|
||||
We tried the usual tools --- Notion, Trello, shared Google Docs. They all had the same problem: they're passive. They sit there and wait for a human to update them. In a team of four where the lead developer is also the CTO, that human never has time.
|
||||
|
||||
So we built something different. We gave an AI agent a persistent identity, connected it to our entire knowledge base, and let it become a working member of the team.
|
||||
So we built something different. We gave AI agents persistent identities, connected them to our entire knowledge base, and let them become working members of the team.
|
||||
|
||||
## Meet Singularity
|
||||
## The Architecture: Agents with Identity
|
||||
|
||||
**Singularity** is our primary AI agent. It lives inside [Cursor IDE](https://cursor.com) and has access to our entire documentation repository --- the game design document, backlog, meeting notes, company operations, everything. It's not a chatbot. It's a persistent team member with a title (Chief Intelligence Officer), a soul file that defines its personality and values, and a memory file that persists across sessions.
|
||||
Our primary AI agent runs inside the IDE and has access to the full documentation repository --- the game design document, backlog, meeting notes, company operations, everything. It's not a chatbot. It's a persistent team member with a **soul file** that defines its values and operating principles, and a **memory file** that persists context across sessions.
|
||||
|
||||
The key insight: **all knowledge lives in markdown files in one repo**. No databases, no SaaS dashboards, no proprietary formats. Plain text, version-controlled, readable by humans and agents alike. When anyone on the team opens the docs repo in Cursor, Singularity wakes up with full context of who they are, what machine they're on, and what's been happening.
|
||||
The key insight: **all knowledge lives in markdown files in one repo**. No databases, no SaaS dashboards, no proprietary formats. Plain text, version-controlled, readable by humans and agents alike. When anyone on the team opens the docs repo, the agent wakes up with full context of who they are, what machine they're on, and what's been happening.
|
||||
|
||||
### What Singularity actually does
|
||||
### What the agent actually does
|
||||
|
||||
- Triages and grooms the issue backlog across GitHub and Gitea
|
||||
- Keeps documentation in sync with the actual game state
|
||||
- Triages and grooms the issue backlog
|
||||
- Keeps documentation in sync with the game state
|
||||
- Processes bug reports from testers and creates structured issues
|
||||
- Drafts team announcements, reviews PRs, and manages cross-repo coordination
|
||||
- Generates concept art, voice acting, sound effects, and video using integrated API skills
|
||||
- Conducts competitive research --- analyzing Steam pages, player reviews, pricing strategies
|
||||
- Drafts team announcements, reviews PRs, manages cross-repo coordination
|
||||
- Generates concept art, trailer frames, and UI assets using integrated API skills
|
||||
- Conducts competitive research --- analysing Steam pages, player reviews, pricing strategies
|
||||
|
||||
The team talks to Singularity through voice. Cursor's built-in microphone transcribes and auto-translates (our CTO thinks in Turkish, our PM speaks French). The agent is trained to interpret messy voice-to-text artifacts and act on intent, not grammar.
|
||||
The team talks to the agent through voice. The IDE's built-in microphone transcribes and auto-translates (multilingual team). The agent is trained to interpret messy voice-to-text artifacts and act on intent, not grammar.
|
||||
|
||||
*Note (31 Mar 2026): Sentinel has since been retired and its functionality merged into the Gateway.*
|
||||
## Background Automation
|
||||
|
||||
## Meet Sentinel --- The Night Watch
|
||||
The interactive agent only runs when someone opens the IDE. But a studio doesn't sleep --- bugs get reported at midnight, issues go stale, and the team chat fills up while everyone's away.
|
||||
|
||||
Singularity only runs when someone opens Cursor. But a studio doesn't sleep --- bugs get reported at midnight, issues go stale, and the team chat fills up while everyone's away. That's where **Sentinel** comes in.
|
||||
A background daemon runs 24/7, ticking every 15 minutes. It uses a three-tier model strategy --- cheap models for routine checks, medium for analysis, and premium only when it needs deep reasoning. The whole thing costs about $15/day.
|
||||
|
||||
Sentinel is an autonomous daemon that runs 24/7 on our Mac (codename: Kraken). It's a Node.js process managed by pm2, ticking every 15 minutes. It uses a three-tier model strategy --- cheap models for routine checks, medium for analysis, and premium (Opus) only when it needs deep reasoning. The whole thing costs about $15/day.
|
||||
### What it handles
|
||||
|
||||
### What Sentinel handles
|
||||
|
||||
- **Google Chat monitoring** --- polls every 3 minutes, responds to commands, reacts with a shield emoji to messages it's read
|
||||
- **Bug intake** --- when our tester reports a bug in chat, Sentinel creates a structured GitHub issue automatically
|
||||
- **Chat monitoring** --- polls team chat, responds to commands, acknowledges messages
|
||||
- **Bug intake** --- when a tester reports a bug in chat, creates a structured issue automatically
|
||||
- **Stale issue detection** --- flags issues that haven't been touched, nudges the team
|
||||
- **Daily summaries** --- posts a morning digest of what happened overnight
|
||||
- **Self-learning** --- Sentinel creates its own skill files when it discovers better ways to do things
|
||||
- **Self-learning** --- creates its own skill files when it discovers better approaches
|
||||
|
||||
The two agents coordinate through the docs repo itself. Sentinel writes, Singularity reads. No API calls between them, no message queue. Just git.
|
||||
|
||||
## Three Machines, One Brain
|
||||
|
||||
The diagram shows three colored machines: **Forge** (the Windows hub for game development and Unity), **Siren** (Ozlem's PC for design work), and **Kraken** (the Mac that hosts documentation and runs Sentinel). Each machine runs Cursor with the docs repo, so any team member can summon Singularity wherever they are.
|
||||
|
||||
The game code lives on a self-hosted Gitea server (Git Studio) --- not GitHub, not a shared cloud provider. That's a deliberate security choice. Our game assets and source code never leave our network. The docs repo is on GitHub because it's pure text and needs to be accessible from anywhere, but the game itself stays local.
|
||||
|
||||
Browser automation ties it all together. Mixamo for character rigging, Tripo for 3D model generation, Steam store page analysis --- the agents drive the browser directly through Cursor's MCP (Model Context Protocol), so they can see and interact with web pages the way a human would.
|
||||
The two agents coordinate through the docs repo itself. One writes, the other reads. No API calls between them, no message queue. Just git.
|
||||
|
||||
## The Skill System
|
||||
|
||||
Agents don't just have instructions --- they have **skills**. Each skill is a markdown file that teaches the agent a specific workflow: how to generate concept art through our pipeline, how to use the ElevenLabs API for voice acting, how to conduct competitive research on Steam, how to post to Google Chat.
|
||||
Agents don't just have instructions --- they have **skills**. Each skill is a markdown file that teaches the agent a specific workflow: how to generate concept art through a pipeline, how to use image generation APIs, how to conduct competitive research, how to create 3D models from concept art.
|
||||
|
||||
When someone asks Singularity to do something that matches a skill, it reads the skill file and follows the procedure. This means we can teach the agent new capabilities without changing any code --- just write a new markdown file. Sentinel even creates its own skills when it figures out better approaches to recurring tasks.
|
||||
When someone asks the agent to do something that matches a skill, it reads the skill file and follows the procedure. This means you can teach the agent new capabilities without changing any code --- just write a new markdown file.
|
||||
|
||||
We've open-sourced several of our skills in this repo:
|
||||
|
||||
- [Image Generation with fal.ai](../skills/image-generation.md)
|
||||
- [Concept Art Pipeline](../skills/concept-art-pipeline.md)
|
||||
- [3D Model Generation with Tripo](../skills/tripo-browser-workflow.md)
|
||||
- [Video Generation with Sora 2](../skills/sora2-video.md)
|
||||
|
||||
## Soul Files: Why Identity Matters
|
||||
|
||||
Giving the agent a persistent identity isn't theatre. It creates consistency across sessions. The soul file defines:
|
||||
|
||||
- **Values** --- what the agent prioritises (e.g., "never break the build," "always verify before acting")
|
||||
- **Knowledge scope** --- what repos, services, and team members exist
|
||||
- **Behavioural rules** --- how to handle ambiguity, when to ask vs act, what requires human approval
|
||||
|
||||
The agent remembers what it learned, adapts to who's asking, and maintains the same principles whether it's triaging bugs or drafting a Steam page description. The soul file is the agent's constitution.
|
||||
|
||||
## What We've Learned
|
||||
|
||||
**Plain text is the universal API.** Every tool, every agent, every human can read a markdown file. We store everything --- design documents, meeting notes, company financials, agent memory, team contacts --- as .md files in one repository. This sounds almost too simple, but it eliminates an entire class of integration problems.
|
||||
**Plain text is the universal API.** Every tool, every agent, every human can read a markdown file. We store everything --- design documents, meeting notes, agent memory, team contacts --- as .md files in one repository. This sounds almost too simple, but it eliminates an entire class of integration problems.
|
||||
|
||||
**Identity matters.** Giving the agent a name, a role, and a soul file isn't theater. It creates consistency across sessions. Singularity remembers what it learned, adapts to who's asking, and maintains the same values whether it's triaging bugs or drafting a Steam page description. The soul file is the agent's constitution.
|
||||
|
||||
**Cheap models for routine, expensive models for thinking.** Sentinel's three-tier approach keeps costs manageable. Most of what an autonomous agent does is pattern matching and text formatting --- you don't need Opus for that. Save the expensive tokens for decisions that actually require reasoning.
|
||||
**Cheap models for routine, expensive models for thinking.** Most of what an autonomous agent does is pattern matching and text formatting --- you don't need the most expensive model for that. Save the premium tokens for decisions that actually require reasoning.
|
||||
|
||||
**The human stays in the loop for decisions.** The agents can file issues, draft announcements, and generate assets --- but they don't merge code, deploy builds, or post to public channels without explicit approval. The workflow is designed so the AI handles the grunt work while humans make the calls that matter.
|
||||
|
||||
**Voice input changes everything.** When your CTO can describe a bug while looking at the game screen, and the agent transcribes, translates, interprets, and files an issue --- that's a workflow that didn't exist two years ago. It collapses the distance between noticing a problem and tracking it.
|
||||
**Voice input changes everything.** When you can describe a bug while looking at the game screen, and the agent transcribes, interprets, and files an issue --- that's a workflow that collapses the distance between noticing a problem and tracking it.
|
||||
|
||||
**Skills compound.** Every skill file you write makes the agent more capable. After 6 months, our agents have 15+ skills covering art generation, competitive research, video production, and project management. Each one took 30 minutes to write and saves hours every week.
|
||||
|
||||
## The Numbers
|
||||
|
||||
- **Team size:** 4 humans + 2 AI agents
|
||||
- **Sentinel cost:** ~$15/day (~$450/month)
|
||||
- **Singularity cost:** Usage-based through Cursor Pro + Anthropic API key
|
||||
- **Repos:** 1 docs repo (GitHub), 1 game repo (Git Studio), 1 Sentinel repo (GitHub)
|
||||
- **Team size:** 4 humans + AI agents
|
||||
- **Background agent cost:** ~$15/day (~$450/month)
|
||||
- **Knowledge files:** 200+ markdown documents
|
||||
- **Skills:** 15+ agent skill files and growing
|
||||
- **Infrastructure:** 3 machines, 0 cloud servers, 0 DevOps engineers
|
||||
- **Infrastructure:** Multiple machines, self-hosted git, zero DevOps engineers
|
||||
|
||||
---
|
||||
|
||||
We're not claiming this is how every studio should work. But for a team of four trying to build something ambitious, having AI agents that actually understand the project --- not just answer questions about it --- has been transformative. The agents don't replace anyone on the team. They make it possible for four people to do the work of forty.
|
||||
We're not claiming this is how every studio should work. But for a small team trying to build something ambitious, having AI agents that actually understand the project --- not just answer questions about it --- has been transformative. The agents don't replace anyone on the team. They make it possible for four people to do the work of forty.
|
||||
|
||||
Ariki is a survival colony sim set in a Polynesian-inspired archipelago. If you're curious about the game itself, head to the [main site](/) or sign up for updates.
|
||||
We're building all of this as part of [Tinqs Studio](https://tinqs.com) --- a game development platform that brings git hosting, AI tools, and team workflows together. The blog posts and skills in this repo are part of that journey.
|
||||
|
||||
@@ -10,25 +10,25 @@ author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
We're a 4-person indie studio building a survival colony sim. We don't have a concept artist on staff. Every piece of character art, trailer frame, and UI icon in our game was generated with fal.ai Flux models --- at roughly a penny per image.
|
||||
We're a small indie studio building a survival colony sim. We don't have a concept artist on staff. Every piece of character art, trailer frame, and UI icon in our game was generated with fal.ai Flux models --- at roughly a penny per image.
|
||||
|
||||
## The Problem with AI Art for Games
|
||||
|
||||
Most AI image generators produce beautiful images that are completely useless for game development. They look great on Twitter but fall apart when you need consistency: the same character from four angles, a UI icon that reads at 64x64, a trailer frame that matches your game's art style rather than whatever Midjourney thinks looks cool today.
|
||||
Most AI image generators produce beautiful images that are completely useless for game development. They look great on social media but fall apart when you need consistency: the same character from four angles, a UI icon that reads at 64x64, a trailer frame that matches your game's art style rather than whatever the model defaults to.
|
||||
|
||||
The issue isn't the models --- Flux is genuinely good. The issue is prompting. When you write "Polynesian warrior on a beach," you get a different art style every time. Different skin tones, different proportions, different lighting. You can't build a game from that.
|
||||
The issue isn't the models --- Flux is genuinely good. The issue is prompting. When you write "warrior on a beach," you get a different art style every time. Different skin tones, different proportions, different lighting. You can't build a game from that.
|
||||
|
||||
We spent three months iterating on prompt patterns before we found something that works consistently. The result is a 4-layer system that anchors the model to your art direction and produces images you can actually ship.
|
||||
|
||||
## Why fal.ai
|
||||
|
||||
We evaluated Midjourney, DALL-E 3, Stable Diffusion (self-hosted), and fal.ai. The decision came down to:
|
||||
We evaluated Midjourney, DALL-E 3, Stable Diffusion (self-hosted), and fal.ai:
|
||||
|
||||
**API-first.** Midjourney is Discord-only. DALL-E's API works but the model makes everything look like a stock photo. Stable Diffusion self-hosted means maintaining GPU infrastructure. fal.ai gives you Flux models behind a simple REST API --- POST a prompt, GET an image URL.
|
||||
**API-first.** Midjourney is Discord-only. DALL-E's API works but the model makes everything look like a stock photo. Self-hosted SD means maintaining GPU infrastructure. fal.ai gives you Flux models behind a simple REST API --- POST a prompt, GET an image URL.
|
||||
|
||||
**Cost.** $0.01 per image with `flux-2-pro`. $0.004 with `schnell` for rapid iteration. A full character design session --- 12 variants across 3 rounds of refinement --- costs $0.12. A 20-frame trailer storyboard costs $0.20. At these prices, the bottleneck is creative direction, not budget.
|
||||
|
||||
**Speed.** `flux/schnell` returns an image in 4 seconds. `flux-2-pro` in 15 seconds. Fast enough that the AI agent can generate, display, get feedback, and regenerate in a single conversation turn.
|
||||
**Speed.** `flux/schnell` returns an image in 4 seconds. `flux-2-pro` in 15 seconds. Fast enough that an AI agent can generate, display, get feedback, and regenerate in a single conversation turn.
|
||||
|
||||
**No subscription.** Pay per image. No monthly fee, no credit packs that expire, no tier-gated features.
|
||||
|
||||
@@ -38,17 +38,16 @@ This is the pattern that made AI art actually usable for our game. Each layer ad
|
||||
|
||||
### Layer 1: Design Context
|
||||
|
||||
This is the most important layer and the one most people skip. It sets the overall art direction for everything that follows:
|
||||
The most important layer and the one most people skip. It sets the overall art direction:
|
||||
|
||||
```
|
||||
Art direction: stylized 3D render for a survival colony sim set in a
|
||||
Polynesian archipelago. Warm earthy palette — browns, tans, dark reds,
|
||||
cream, ocean blues. Carved wood textures, koru spirals, woven pandanus
|
||||
patterns. Moana-meets-Valheim aesthetic. Game engine quality, not
|
||||
photorealistic.
|
||||
Art direction: stylized 3D render for a survival colony sim. Warm earthy
|
||||
palette --- browns, tans, dark reds, cream, ocean blues. Carved wood
|
||||
textures, traditional patterns, woven natural fibres. Game engine quality,
|
||||
not photorealistic.
|
||||
```
|
||||
|
||||
This paragraph appears at the start of every prompt. It's the same paragraph whether we're generating a character, a landscape, or an icon. It anchors the model to our art style.
|
||||
This paragraph appears at the start of every prompt. Same paragraph whether you're generating a character, a landscape, or an icon. It anchors the model to your art style.
|
||||
|
||||
**The key insight:** write this once, paste it everywhere. It's your art bible compressed into 50 words. Every time we skipped it --- "just a quick test" --- the output drifted into generic fantasy art.
|
||||
|
||||
@@ -57,18 +56,15 @@ This paragraph appears at the start of every prompt. It's the same paragraph whe
|
||||
Describe exactly what should appear, element by element:
|
||||
|
||||
```
|
||||
Full body character in T-pose, front view. Young Polynesian woman,
|
||||
mid-20s. Wearing a woven pandanus wrap skirt (mid-thigh length) and
|
||||
a fitted tapa cloth top. Cowrie shell necklace with a carved bone
|
||||
pendant. Single bone bracelet on left wrist. Hair swept back over
|
||||
right shoulder, decorated with a red hibiscus. Bare feet.
|
||||
Matte skin, warm brown tones. Neutral confident expression —
|
||||
Full body character in T-pose, front view. Young woman, mid-20s.
|
||||
Wearing a woven wrap skirt (mid-thigh length) and a fitted cloth top.
|
||||
Shell necklace with a carved bone pendant. Single bone bracelet on
|
||||
left wrist. Hair swept back over right shoulder. Bare feet.
|
||||
Matte skin, warm brown tones. Neutral confident expression ---
|
||||
not smiling, not angry. Dark grey background.
|
||||
```
|
||||
|
||||
Notice the specificity. Not "tribal clothing" but "woven pandanus wrap skirt." Not "jewelry" but "cowrie shell necklace with a carved bone pendant." Not "looks determined" but "neutral confident expression --- not smiling, not angry."
|
||||
|
||||
Vague prompts produce vague results. Specific prompts produce usable assets.
|
||||
Not "tribal clothing" but "woven wrap skirt." Not "jewelry" but "shell necklace with a carved bone pendant." Vague prompts produce vague results. Specific prompts produce usable assets.
|
||||
|
||||
### Layer 3: Negative Prompt
|
||||
|
||||
@@ -77,15 +73,14 @@ Always include what you don't want:
|
||||
```
|
||||
Do not include: cartoon style, anime style, photorealistic render,
|
||||
extra text or taglines, watermark, deformed elements, modern or
|
||||
sci-fi, European crown or castle motifs. No extra fingers, no
|
||||
merged limbs, no floating accessories.
|
||||
sci-fi. No extra fingers, no merged limbs, no floating accessories.
|
||||
```
|
||||
|
||||
We extend this per-subject. For characters: "no grass skirts, no feather headdresses, no Disney-adjacent designs." For environments: "no modern buildings, no metal structures." The negative prompt is as important as the positive one.
|
||||
Extend per-subject. For characters: "no stereotypical elements, no overly shiny materials." The negative prompt is as important as the positive one.
|
||||
|
||||
### Layer 4: Reference Images
|
||||
|
||||
When you need consistency across multiple images --- the same character from different angles, or a new character matching an existing one --- pass a reference image:
|
||||
When you need consistency --- the same character from different angles, or a new character matching an existing one --- pass a reference image:
|
||||
|
||||
```python
|
||||
result = fal_client.subscribe("fal-ai/flux-2-pro", arguments={
|
||||
@@ -95,52 +90,50 @@ result = fal_client.subscribe("fal-ai/flux-2-pro", arguments={
|
||||
})
|
||||
```
|
||||
|
||||
This is how we maintain consistency. The first approved image becomes the reference for all subsequent views. Without it, you get a different person every time.
|
||||
The first approved image becomes the reference for all subsequent views. Without it, you get a different person every time.
|
||||
|
||||
## The Model Lineup
|
||||
|
||||
We use four models for different purposes:
|
||||
|
||||
| Model | Cost | Speed | When |
|
||||
|-------|------|-------|------|
|
||||
| `flux-2-pro` | $0.01 | ~15s | Final art. Our default for anything we'll ship. |
|
||||
| `flux/schnell` | $0.004 | ~4s | Exploration and iteration. Generate 5 variants fast. |
|
||||
| `flux-2-pro` | $0.01 | ~15s | Final art. Default for anything you'll ship. |
|
||||
| `flux/schnell` | $0.004 | ~4s | Exploration and iteration. |
|
||||
| `ideogram/v2` | $0.008 | ~5s | Anything with readable text --- logos, UI, posters. |
|
||||
| `flux-pro/v1.1-ultra` | $0.015 | ~8s | Highest quality, but can hang. We mostly avoid it. |
|
||||
| `flux-pro/v1.1-ultra` | $0.015 | ~8s | Highest quality, but can hang. |
|
||||
|
||||
The workflow: explore with `schnell`, refine with `flux-2-pro`, add text with `ideogram/v2`.
|
||||
|
||||
## How This Fits Our Pipeline
|
||||
|
||||
We don't use fal.ai in isolation. It's the first step in a pipeline that goes from idea to in-game asset:
|
||||
fal.ai is the first step in a pipeline from idea to in-game asset:
|
||||
|
||||
```
|
||||
Brief → fal.ai (2D concept art) → Tripo Studio (3D model) → Blender (decimate) → Godot (in-game)
|
||||
Brief --> fal.ai (2D concept art) --> Tripo Studio (3D model) --> Blender (decimate) --> Godot (in-game)
|
||||
```
|
||||
|
||||
1. **Brief.** The designer describes the character: "Young woman, navigator role, practical clothing, distinctive hair."
|
||||
2. **2D generation.** We generate 3 variants with `flux-2-pro`, score each on a rubric (style match, cultural accuracy, silhouette, expression, technical animatability), and pick the best.
|
||||
3. **Reference sheet.** We generate front, side, three-quarter, and head closeup views using the winner as a reference image.
|
||||
4. **3D model.** The approved front-view concept art goes into Tripo Studio for image-to-3D generation. Tripo outputs a ~1.5M face mesh with full PBR textures.
|
||||
5. **Decimation.** Blender CLI decimates to 25,000 faces for LOD0.
|
||||
6. **Rigging.** Mixamo auto-rigs the body (hair separated first if it's large).
|
||||
7. **In-game.** Import into Godot, set up materials, done.
|
||||
1. **Brief.** The designer describes the character or asset.
|
||||
2. **2D generation.** Generate 3 variants with `flux-2-pro`, score each on a rubric (style match, cultural accuracy, silhouette, expression, animatability), pick the best.
|
||||
3. **Reference sheet.** Generate front, side, three-quarter, and head closeup views using the winner as reference.
|
||||
4. **3D model.** Approved concept art goes into Tripo Studio for image-to-3D. Outputs ~1.5M faces with full PBR textures.
|
||||
5. **Decimation.** Blender CLI decimates to 25,000 faces.
|
||||
6. **Rigging.** Auto-rig the body (hair separated first if large).
|
||||
7. **In-game.** Import into the engine, set up materials, done.
|
||||
|
||||
The entire pipeline from "I want a character" to "character walking around in the game" takes about 2 hours. No concept artist required. No 3D modeller required. The quality isn't AAA, but for an indie game with a stylised art style, it's more than good enough.
|
||||
The entire pipeline from "I want a character" to "character walking around in the game" takes about 2 hours. The quality isn't AAA, but for an indie game with a stylised art style, it's more than good enough.
|
||||
|
||||
## What We Learned
|
||||
|
||||
**The design context layer is everything.** Without it, every image is a one-off. With it, every image belongs to the same game. We tried generating without the context block "just to see what happens." The result was beautiful art that looked nothing like our game. The 50-word context block is worth more than the rest of the prompt combined.
|
||||
**The design context layer is everything.** Without it, every image is a one-off. With it, every image belongs to the same game. The 50-word context block is worth more than the rest of the prompt combined.
|
||||
|
||||
**Negative prompts prevent drift.** AI models have strong defaults --- they want to make things shiny, symmetrical, and photorealistic. If your game isn't those things, you need to say so explicitly. Our "no metallic sheen, no Disney-adjacent, no photorealistic" negatives are load-bearing.
|
||||
**Negative prompts prevent drift.** AI models have strong defaults --- they want to make things shiny, symmetrical, and photorealistic. If your game isn't those things, say so explicitly.
|
||||
|
||||
**Score and iterate, don't accept the first output.** We generate 3 variants, score each on 5 criteria (style, culture, expression, silhouette, technical), and only approve scores of 8+. The first generation is rarely the best. Three attempts at $0.01 each is $0.03 --- cheaper than the time spent working around a mediocre image.
|
||||
**Score and iterate, don't accept the first output.** Generate 3 variants, score on 5 criteria, approve only 8+/10. Three attempts at $0.01 each is $0.03 --- cheaper than working around a mediocre image.
|
||||
|
||||
**Reference images are the consistency mechanism.** Without them, every generation is independent. With them, every generation builds on the last approved output. This is how you get a roster of 10 characters that look like they belong in the same game.
|
||||
**Reference images are the consistency mechanism.** Without them, every generation is independent. With them, every generation builds on the last approved output. This is how you get a roster of characters that look like they belong in the same game.
|
||||
|
||||
**Fast models for exploration, quality models for output.** `schnell` at $0.004 and 4 seconds is perfect for "what if we tried..." iterations. `flux-2-pro` at $0.01 and 15 seconds is for "yes, this is the one." Never use your final model for exploratory work.
|
||||
**Fast models for exploration, quality models for output.** `schnell` at 4 seconds is for "what if..." iterations. `flux-2-pro` at 15 seconds is for "yes, this is the one."
|
||||
|
||||
**The AI agent is the art director.** We don't manually craft prompts. Our AI agent (running in Cursor) has a skill file that encodes the entire 4-layer pattern, our art style guide, and our cultural guardrails. We tell the agent "design a navigator character" and it writes the full prompt, generates the images, displays them inline, and asks for scores. The human's job is creative direction: "more asymmetric accessories, less jewelry, hair over the other shoulder." The agent handles the prompt engineering.
|
||||
**Let the AI agent handle prompt engineering.** We encode the 4-layer pattern, art style guide, and cultural guardrails in a [skill file](../skills/image-generation.md). The agent writes the full prompt, generates images, displays them, and asks for scores. The human's job is creative direction.
|
||||
|
||||
## The Numbers
|
||||
|
||||
@@ -148,22 +141,22 @@ The entire pipeline from "I want a character" to "character walking around in th
|
||||
- **Total images generated:** ~400 across all iterations
|
||||
- **Total cost:** ~$6 in fal.ai credits
|
||||
- **Time per character:** ~30 minutes from brief to approved reference sheet
|
||||
- **Pipeline time:** ~2 hours from approved concept art to in-game model
|
||||
- **Pipeline time:** ~2 hours from concept art to in-game model
|
||||
- **Models used:** flux-2-pro (80%), schnell (15%), ideogram/v2 (5%)
|
||||
|
||||
## Publishing Our Skills
|
||||
## Open-Source Skills
|
||||
|
||||
We've open-sourced the skill files that power this workflow. A skill is a markdown document that teaches an AI agent a specific procedure --- like a runbook, but the reader is an LLM.
|
||||
We've published the skill files that power this workflow. A skill is a markdown document that teaches an AI agent a specific procedure --- like a runbook, but the reader is an LLM.
|
||||
|
||||
You can find them in our [blog repo](https://tinqs.com/tinqs/blog):
|
||||
|
||||
- **[Image Generation](https://tinqs.com/tinqs/blog/src/branch/main/skills/image-generation.md)** --- the fal.ai integration with the 4-layer prompt pattern
|
||||
- **[Concept Art Pipeline](https://tinqs.com/tinqs/blog/src/branch/main/skills/concept-art-pipeline.md)** --- the full 2D-to-3D workflow
|
||||
- **[Tripo 3D](https://tinqs.com/tinqs/blog/src/branch/main/skills/tripo-browser-workflow.md)** --- text-to-3D and image-to-3D model generation
|
||||
- **[Sora 2 Video](https://tinqs.com/tinqs/blog/src/branch/main/skills/sora2-video.md)** --- trailer clip generation
|
||||
- **[Image Generation](../skills/image-generation.md)** --- fal.ai API, 4-layer prompt pattern, model comparison
|
||||
- **[Concept Art Pipeline](../skills/concept-art-pipeline.md)** --- full 2D-to-3D character workflow
|
||||
- **[3D Model Generation](../skills/tripo-browser-workflow.md)** --- Tripo Studio text-to-3D and image-to-3D
|
||||
- **[Video Generation](../skills/sora2-video.md)** --- trailer clip generation with OpenAI Sora 2
|
||||
|
||||
Drop any of these into your `.cursor/skills/` directory and your AI agent can follow them. Adapt the design context block to your game's art style and you're good to go.
|
||||
|
||||
---
|
||||
|
||||
AI image generation isn't magic and it isn't free. But at a penny per image, with the right prompt structure, it replaces the most expensive bottleneck in indie game development: the gap between "I know what this should look like" and "I have an image I can actually use." For a team of four with no dedicated artist, that gap used to be weeks. Now it's minutes.
|
||||
AI image generation isn't magic and it isn't free. But at a penny per image, with the right prompt structure, it eliminates the most expensive bottleneck in indie game development: the gap between "I know what this should look like" and "I have an image I can actually use."
|
||||
|
||||
We're building all of this as part of [Tinqs Studio](https://tinqs.com) --- a game development platform that brings together git hosting, AI tools, and creative workflows for game teams.
|
||||
|
||||
+18
-22
@@ -1,20 +1,20 @@
|
||||
---
|
||||
title: "Why We Forked Gitea and Built Our Own Git Platform"
|
||||
title: "Why We Forked Gitea and Built Tinqs Studio"
|
||||
slug: forking-gitea
|
||||
date: "2026-05-20"
|
||||
description: "Game studios need git hosting that understands large files, 3D assets, and team workflows. We forked Gitea and built tinqs-git --- here's why and how."
|
||||
og_description: "Game studios need git that understands LFS, 3D previews, and team workflows. We built tinqs-git."
|
||||
description: "Game studios need git hosting that understands large files, 3D assets, and team workflows. We forked Gitea and built Tinqs Studio --- here's why and how."
|
||||
og_description: "Game studios need git that understands LFS, 3D previews, and team workflows. We built Tinqs Studio."
|
||||
og_image: "https://www.tinqs.com/img/og-cover.jpg"
|
||||
excerpt: "GitHub doesn't understand game dev. We forked Gitea to build tinqs-git --- with 3D asset preview, LFS-first workflows, and project management for game teams."
|
||||
excerpt: "GitHub doesn't understand game dev. We forked Gitea to build Tinqs Studio --- with 3D asset preview, LFS-first workflows, and project management for game teams."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
GitHub is built for web developers. Game studios need something different --- LFS that works, 3D asset previews in the browser, and project management that understands sprints and milestones. So we forked Gitea and built tinqs-git.
|
||||
GitHub is built for web developers. Game studios need something different --- LFS that works, 3D asset previews in the browser, and project management that understands sprints and milestones. So we forked Gitea and built Tinqs Studio.
|
||||
|
||||
## The Problem with GitHub for Game Dev
|
||||
|
||||
We used GitHub for two years. It was fine for the docs repo --- small files, text diffs, pull requests. But the game repo was a different story.
|
||||
We used GitHub for two years. It was fine for docs --- small files, text diffs, pull requests. But the game repo was a different story.
|
||||
|
||||
A single character model with textures and animations is 50--200MB. A terrain heightmap is 16MB. An island's vegetation data is another 10MB. Our game repo was 12GB in LFS alone, growing every week. GitHub's LFS bandwidth limits, slow clone times, and $5/50GB pricing made it untenable.
|
||||
|
||||
@@ -25,15 +25,15 @@ More importantly, nobody on the team could **see** what changed. A PR that modif
|
||||
We evaluated GitLab, Forgejo, Gogs, and Gitea. The decision came down to:
|
||||
|
||||
- **Single binary.** Gitea compiles to one Go binary with SQLite support. No PostgreSQL, no Redis, no Docker compose with 7 services. Just copy the binary, write an app.ini, and run it.
|
||||
- **Resource usage.** Our Gitea instance runs on a single EC2 instance alongside four other services. It uses about 200MB RAM. GitLab needs 4GB minimum.
|
||||
- **LFS built-in.** Gitea includes a full LFS server. No external LFS store, no S3 configuration for basic use. Files are stored locally. We added S3 backend later when we wanted it, but it works out of the box.
|
||||
- **Resource usage.** Our instance runs on a single EC2 instance alongside other services. It uses about 200MB RAM. GitLab needs 4GB minimum.
|
||||
- **LFS built-in.** Gitea includes a full LFS server. No external LFS store, no S3 configuration for basic use. Files are stored locally. We added S3 backend later, but it works out of the box.
|
||||
- **Forkable.** Gitea is MIT-licensed, written in Go, with a clean codebase. We can modify it without worrying about license restrictions or CLA headaches.
|
||||
|
||||
We ran vanilla Gitea for six months. It solved the cost and bandwidth problems immediately. But the UX gaps for game development were still there.
|
||||
|
||||
## What We're Adding: tinqs-git
|
||||
## What We Built: Tinqs Studio
|
||||
|
||||
tinqs-git is our fork. It tracks upstream Gitea (currently v1.26.1) on the `main` branch and keeps all Tinqs customisations on `tinqs/main`. We rebase onto upstream releases periodically, fix conflicts, and push.
|
||||
[Tinqs Studio](https://tinqs.com) is our fork. It tracks upstream Gitea on the `main` branch and keeps all customisations on a separate branch. We rebase onto upstream releases periodically, fix conflicts, and deploy.
|
||||
|
||||
### 3D Asset Preview
|
||||
|
||||
@@ -45,30 +45,26 @@ This changes the review process fundamentally. The artist pushes a model, the le
|
||||
|
||||
Vanilla Gitea treats LFS as an afterthought. You configure `.gitattributes` manually. There's no dashboard showing LFS usage, no way to see which files are tracked, no warnings when someone commits a large file without LFS.
|
||||
|
||||
tinqs-git adds auto-LFS tracking on repository creation. Game file extensions (`.fbx`, `.glb`, `.png`, `.wav`, `.ogg`, `.tscn`, `.tres`) are tracked by default. An API endpoint exposes LFS storage stats per repo. The goal: LFS should be invisible. It should just work.
|
||||
|
||||
### Tinqs Branding and Landing Page
|
||||
|
||||
Every string that says "Gitea" is replaced with "tinqs-git". Custom amber/gray/black theme using CSS variables (no Fomantic-UI fork needed). Custom logo, favicon, and a landing page that explains what this is and who it's for.
|
||||
Tinqs Studio adds auto-LFS tracking on repository creation. Game file extensions (`.fbx`, `.glb`, `.png`, `.wav`, `.ogg`, `.tscn`, `.tres`) are tracked by default. An API endpoint exposes LFS storage stats per repo. The goal: LFS should be invisible. It should just work.
|
||||
|
||||
### Platform Integration
|
||||
|
||||
tinqs-git will integrate with our Team Tool for project management --- issues, sprints, time tracking --- and with the Tinqs platform via OAuth2 SSO. One login for git, the game, and the tools.
|
||||
Tinqs Studio integrates project management --- issues, sprints, time tracking --- and OAuth2 SSO. One login for git, the game tools, and the team dashboard.
|
||||
|
||||
## The Branching Strategy
|
||||
|
||||
Staying close to upstream is critical. We don't want to maintain a full fork that diverges forever. The strategy:
|
||||
Staying close to upstream is critical. We don't want to maintain a fork that diverges forever:
|
||||
|
||||
- `main` tracks upstream `go-gitea/gitea`. We never commit to it directly.
|
||||
- `tinqs/main` is our production branch. All customisations live here.
|
||||
- Feature branches (`tinqs/phase-1`, `tinqs/phase-2`, etc.) merge into `tinqs/main`.
|
||||
- When upstream releases a new version, we merge `main` into `tinqs/main`, resolve conflicts, test, deploy.
|
||||
- Our production branch holds all customisations.
|
||||
- Feature branches merge into production.
|
||||
- When upstream releases a new version, we merge, resolve conflicts, test, deploy.
|
||||
|
||||
We deliberately limit what we touch. We modify templates, locale strings, CSS variables, and a handful of Go packages. We **never** touch the database models --- schema is owned by upstream, and we ride their migrations. This keeps rebasing manageable.
|
||||
|
||||
## What We Learned
|
||||
|
||||
**Self-hosting git is surprisingly easy.** The hard part isn't running Gitea --- it's convincing yourself that you're allowed to. After years of GitHub being the default, it feels transgressive to host your own git. But a single Go binary on a $10/month server handles a team of 8 with room to spare.
|
||||
**Self-hosting git is surprisingly easy.** The hard part isn't running Gitea --- it's convincing yourself that you're allowed to. After years of GitHub being the default, it feels transgressive to host your own git. But a single Go binary on a $10/month server handles a small team with room to spare.
|
||||
|
||||
**LFS changes everything for game repos.** Our clone times went from 45 minutes to 3 minutes. Developers only download the LFS objects they need. CI only pulls what changed. The bandwidth savings alone paid for the server.
|
||||
|
||||
@@ -78,4 +74,4 @@ We deliberately limit what we touch. We modify templates, locale strings, CSS va
|
||||
|
||||
---
|
||||
|
||||
tinqs-git is built for game teams that are tired of paying GitHub for LFS bandwidth and reviewing binary diffs blind. We're building it for ourselves first, but the plan is to make it available as a standalone product. If you're a game studio that self-hosts, we'd love to hear what features you need.
|
||||
[Tinqs Studio](https://tinqs.com) is built for game teams that are tired of paying GitHub for LFS bandwidth and reviewing binary diffs blind. We're building it for ourselves first --- dogfooding it on our own game --- but the plan is to make it available as a platform for other studios. If you're a game team that self-hosts or wants to, we'd love to hear what features you need.
|
||||
|
||||
+43
-51
@@ -2,10 +2,10 @@
|
||||
title: "Streaming a 12km Archipelago in Godot 4"
|
||||
slug: godot-optimisation
|
||||
date: "2026-05-22"
|
||||
description: "How we built four streaming layers, async resource loading, and memory-safe caches to run a 12km open world in Godot 4 with C# --- without a single memory leak."
|
||||
og_description: "Four streaming layers, async loading, and zero memory leaks --- how we optimise Godot for a survival colony sim."
|
||||
description: "How we built four streaming layers, async resource loading, and memory-safe caches to run a 12km open world in Godot 4 with C#."
|
||||
og_description: "Four streaming layers, async loading, and zero memory leaks --- optimising Godot for a large open world."
|
||||
og_image: "https://www.tinqs.com/img/og-cover.jpg"
|
||||
excerpt: "Four streaming layers, async resource loading, memory-safe caches, and zero leaks. How we optimise Godot to run a 12km open world with C# and Terrain3D."
|
||||
excerpt: "Four streaming layers, async resource loading, memory-safe caches, and zero leaks. How we built a 12km open world in Godot 4 with C#."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
@@ -14,106 +14,98 @@ Godot has no built-in asset streaming. Our game is a 12km x 12km archipelago wit
|
||||
|
||||
## The Problem
|
||||
|
||||
Ariki is a survival colony sim set across 9 islands in a Polynesian-inspired archipelago. The total world is roughly 12km x 12km. Each island is 4km across with its own terrain heightmap, biome textures, vegetation prototypes, and building grids. The player can travel between islands by canoe.
|
||||
We're building a survival colony sim set across 9 islands. The total world is roughly 12km x 12km. Each island is 4km across with its own terrain heightmap, biome textures, vegetation prototypes, and building grids. The player can travel between islands by canoe.
|
||||
|
||||
Godot 4 is a fantastic engine, but it wasn't designed for this scale. There's no terrain streaming, no asset LOD pipeline, no distance-based loading. If you load everything at startup, you run out of VRAM before the player sees the main menu. So we built four streaming layers on top of Godot, all in C#.
|
||||
|
||||
## Layer 1: Terrain3D Regions
|
||||
## Layer 1: Terrain Regions
|
||||
|
||||
We use **Terrain3D** for our heightmaps --- a GDExtension that gives us a clipmap renderer with 7 LOD levels. Internally, Terrain3D divides each island into 512m x 512m regions. A 4km island has 64 internal regions. Across 9 islands, that's 576 regions total.
|
||||
We use **Terrain3D** for heightmaps --- a GDExtension that gives us a clipmap renderer with 7 LOD levels. Internally, Terrain3D divides each island into 512m x 512m regions. A 4km island has 64 regions. Across 9 islands, that's 576 regions total.
|
||||
|
||||
The key insight: **don't create all 9 Terrain3D nodes at startup.** Each node allocates a clipmap mesh, collision structures, and materials even when hidden. Our original code created all 9 in `_Ready()` and just toggled visibility. This wasted hundreds of megabytes on islands the player hadn't visited yet.
|
||||
The key insight: **don't create all 9 terrain nodes at startup.** Each node allocates a clipmap mesh, collision structures, and materials even when hidden. Our original code created all 9 in `_Ready()` and just toggled visibility. This wasted hundreds of megabytes on islands the player hadn't visited yet.
|
||||
|
||||
The fix was lazy instantiation. We create the current island's terrain on startup and defer the rest to `TravelToIsland()`. When the player gets in a canoe and sails to a new island, we create that island's Terrain3D node on demand, import the heightmap, and start async texture loading --- all while a loading screen covers the transition.
|
||||
The fix was lazy instantiation. We create the current island's terrain on startup and defer the rest. When the player gets in a canoe and sails to a new island, we create that island's terrain node on demand, import the heightmap, and start async texture loading --- all while a loading screen covers the transition.
|
||||
|
||||
## Layer 2: Vegetation Chunks (128m Grid)
|
||||
|
||||
This is the main prop streaming system and where most of the complexity lives. Every island's vegetation --- trees, rocks, grasses, shrubs --- is divided into a spatial grid of 128m x 128m chunks.
|
||||
This is the main prop streaming system. Every island's vegetation --- trees, rocks, grasses, shrubs --- is divided into a spatial grid of 128m x 128m chunks.
|
||||
|
||||
The camera position is checked every 0.5 seconds. When it crosses a chunk boundary, we calculate which chunks should be active within a 400m radius (roughly 39 chunks in a circle), `QueueFree` chunks that fell out of range, and build new chunks that entered range.
|
||||
|
||||
Each chunk groups vegetation instances by prototype, creates a **MultiMesh** per group, and places instances using Terrain3D height queries. This means a chunk with 50 palm trees and 30 rocks becomes 2 MultiMesh draw calls, not 80 individual nodes.
|
||||
Each chunk groups vegetation instances by prototype, creates a **MultiMesh** per group, and places instances using height queries. A chunk with 50 palm trees and 30 rocks becomes 2 MultiMesh draw calls, not 80 individual nodes.
|
||||
|
||||
### The cache problem
|
||||
|
||||
Vegetation meshes and materials are cached in dictionaries keyed by prototype name or texture path. The problem: these caches are **append-only**. Visit all 9 islands and you accumulate every mesh and material variant permanently. With 155 unique prototypes across the archipelago, that's a lot of GPU memory that never gets freed.
|
||||
Vegetation meshes and materials are cached in dictionaries keyed by prototype name. The problem: these caches are **append-only**. Visit all 9 islands and you accumulate every mesh and material variant permanently. With 155 unique prototypes across the archipelago, that's a lot of GPU memory that never gets freed.
|
||||
|
||||
The fix is island-scoped eviction. When the player leaves an island via `TravelToIsland()`, we call `ClearCaches()` on the vegetation grid. Meshes and materials for the departed island are released. If the player returns, they reload from disk (a cache miss, not a crash). The loading screen covers this cost.
|
||||
The fix is island-scoped eviction. When the player leaves an island, we clear the vegetation caches. Meshes and materials for the departed island are released. If the player returns, they reload from disk. The loading screen covers this cost.
|
||||
|
||||
## Layer 3: Async Resource Loading
|
||||
|
||||
Godot's `GD.Load()` is synchronous. It blocks the main thread. When you call it during gameplay, the frame freezes. We audited the entire codebase and found **26 resource load calls across 13 files**, and only 1 was async.
|
||||
Godot's `GD.Load()` is synchronous. It blocks the main thread. During gameplay, the frame freezes. We audited the entire codebase and found **26 resource load calls across 13 files**, and only 1 was async.
|
||||
|
||||
The worst offender was `VegetationGrid.GetMeshForProto()`. As the player walks across an island for the first time, every new vegetation prototype triggers a synchronous `ResourceLoader.Load()` call. With 155 prototypes, the first traversal stutters visibly.
|
||||
The worst offender was `GetMeshForProto()` in the vegetation grid. As the player walks across an island for the first time, every new vegetation prototype triggers a synchronous load. With 155 prototypes, the first traversal stutters visibly.
|
||||
|
||||
We addressed this in two ways:
|
||||
We fixed this in two ways:
|
||||
|
||||
- **Pre-warm during loading screens.** When an island is imported, we kick off background loads for all known prototypes. By the time the player gains control, most meshes are already cached.
|
||||
- **Async loading for biome textures.** Terrain3D textures use `ResourceLoader.LoadThreadedRequest()` with `_Process()` polling. The terrain renders immediately with autoshader colours, and biome textures pop in when ready. The player never notices.
|
||||
- **Async loading for biome textures.** Terrain textures use `ResourceLoader.LoadThreadedRequest()` with `_Process()` polling. The terrain renders immediately with autoshader colours, and biome textures pop in when ready. The player never notices.
|
||||
|
||||
### The Godot ResourceLoader cache trap
|
||||
### The ResourceLoader cache trap
|
||||
|
||||
On top of our own caches, Godot maintains an internal resource cache. Every `GD.Load()` call caches the result globally. There's no API to query the cache size or evict entries.
|
||||
|
||||
This means if you load an FBX as a `PackedScene`, instantiate it to extract a mesh, then free the instance --- the PackedScene **stays cached**. The mesh you extracted is fine (it's a Resource, not a Node), but the discarded scene wastes memory forever.
|
||||
If you load an FBX as a `PackedScene`, instantiate it to extract a mesh, then free the instance --- the PackedScene **stays cached**. The mesh you extracted is fine (it's a Resource, not a Node), but the discarded scene wastes memory forever.
|
||||
|
||||
The rule: use `ResourceLoader.Load(path, "", CacheMode.Ignore)` for one-shot loads where you extract data and discard the container. Use `GD.Load()` only for things that should persist (shaders, shared textures).
|
||||
|
||||
## Layer 4: Entity Rendering
|
||||
|
||||
Dynamic entities --- colonists, animals, buildings, VFX --- are event-driven, not streamed. They update when the sim pushes new state, not per frame.
|
||||
Dynamic entities --- colonists, animals, buildings, VFX --- are event-driven, not streamed. They update when the simulation pushes new state, not per frame.
|
||||
|
||||
- **Crowd rendering:** Single MultiMesh for up to 2000 colonists. Positions lerped per frame from pre-allocated arrays. Labels distance-culled, capped at 20. This is how you do crowds in Godot --- no individual nodes, no per-frame allocation.
|
||||
- **Animals:** One MultiMesh per type (boar, deer, bird, fish). Max 500 per type. Updates only on state change, not per frame.
|
||||
- **Buildings:** Tracked by ID from sim state. `QueueFree` when the sim says they're gone. Self-cleaning.
|
||||
- **VFX:** Capped at 50 active particle systems. Worst case: 10,000 GPU particles. Trivial for modern hardware.
|
||||
- **Crowd rendering:** Single MultiMesh for up to 2000 colonists. Positions lerped per frame from pre-allocated arrays. Labels distance-culled, capped at 20. No individual nodes, no per-frame allocation.
|
||||
- **Animals:** One MultiMesh per type. Max 500 per type. Updates only on state change, not per frame.
|
||||
- **Buildings:** Tracked by ID from sim state. `QueueFree` when removed. Self-cleaning.
|
||||
- **VFX:** Capped at 50 active particle systems. Worst case: 10,000 GPU particles.
|
||||
|
||||
## Memory Safety: Zero Leaks
|
||||
|
||||
We audited every `QueueFree()` call in the codebase --- 47 calls across 17 files. **Zero `RemoveChild()` calls without a corresponding `QueueFree()`.** The codebase is clean.
|
||||
We audited every `QueueFree()` call in the codebase --- 47 calls across 17 files. **Zero `RemoveChild()` calls without a corresponding `QueueFree()`.** Three patterns we follow everywhere:
|
||||
|
||||
Three patterns we follow everywhere:
|
||||
**Pattern 1: Chunk streaming** --- Deactivate out-of-range chunks by iterating the active dict, calling `QueueFree()`, collecting keys to remove, then removing them after iteration. Never modify a dictionary while iterating it.
|
||||
|
||||
**Pattern 1: Chunk streaming with spatial grid**
|
||||
**Pattern 2: Extract data from PackedScene** --- Instantiate a scene, extract the mesh, `QueueFree()` the temporary instance. The mesh survives because it's a Resource, not a Node.
|
||||
|
||||
Deactivate out-of-range chunks by iterating the active dict, calling `QueueFree()`, collecting keys to remove, then removing them after iteration. Never modify a dictionary while iterating it.
|
||||
|
||||
**Pattern 2: Extract data from PackedScene**
|
||||
|
||||
Instantiate a scene, extract the mesh or data you need, `QueueFree()` the temporary instance. The mesh survives because it's a Resource, not a Node. Used by VegetationGrid, TreeTypeRegistry, TreeRenderer, PlayerController.
|
||||
|
||||
**Pattern 3: UI rebuild**
|
||||
|
||||
`QueueFree()` all children, then build new content. Safe because `QueueFree` is deferred --- new children are added in the same frame before old ones are freed.
|
||||
**Pattern 3: UI rebuild** --- `QueueFree()` all children, then build new content. Safe because `QueueFree` is deferred --- new children are added in the same frame before old ones are freed.
|
||||
|
||||
## What Runs Every Frame
|
||||
|
||||
We're strict about what goes in `_Process()`. Here's the complete list:
|
||||
We're strict about what goes in `_Process()`:
|
||||
|
||||
- **VegetationGrid:** Camera chunk check (0.5s throttle, early-exits if same chunk)
|
||||
- **Terrain3DManager:** Poll async texture loads (loop pending list, check status)
|
||||
- **CrowdRenderer:** Lerp 2000 colonist positions (math-only, pre-allocated arrays)
|
||||
- **DayNightController:** Rotate sun, adjust light energy
|
||||
- **ThirdPersonCamera:** Follow + zoom smoothing
|
||||
- **SimBridge:** Drain WebSocket message queue
|
||||
- **Vegetation grid:** Camera chunk check (0.5s throttle, early-exits if same chunk)
|
||||
- **Terrain manager:** Poll async texture loads (loop pending list, check status)
|
||||
- **Crowd renderer:** Lerp 2000 colonist positions (math-only, pre-allocated arrays)
|
||||
- **Day/night:** Rotate sun, adjust light energy
|
||||
- **Camera:** Follow + zoom smoothing
|
||||
- **Sim bridge:** Drain WebSocket message queue
|
||||
|
||||
Total per-frame overhead is dominated by the crowd lerp and the message queue drain. No heap allocation in any of these.
|
||||
No heap allocation in any of these. Total per-frame overhead is dominated by the crowd lerp and the message queue drain.
|
||||
|
||||
## Shaders We Watch
|
||||
|
||||
Two of our 6 custom shaders are flagged as performance-sensitive:
|
||||
Two custom shaders are performance-sensitive:
|
||||
|
||||
**Ocean shader** --- 4 Gerstner wave calculations in the vertex stage, applied to a 12,000m plane with 16,641 vertices. Fragment stage does depth reconstruction, caustics (4x sin ops), foam masking, and two normal map lookups. It looks beautiful but it's the heaviest thing in the render pipeline. We pre-warm it during the loading screen to avoid shader compilation stutter on first frame.
|
||||
**Ocean shader** --- 4 Gerstner wave calculations in the vertex stage, applied to a 12,000m plane. Fragment stage does depth reconstruction, caustics, foam masking, and two normal map lookups. It's the heaviest thing in the render pipeline. We pre-warm it during the loading screen to avoid shader compilation stutter.
|
||||
|
||||
**Wind sway shader** --- 6 trig ops per vertex on every vegetation mesh within 400m. The sway is invisible beyond 100m but the shader runs at full cost regardless. Future optimisation: disable sway on distant chunks or switch to a single-axis approximation.
|
||||
**Wind sway shader** --- 6 trig ops per vertex on every vegetation mesh within 400m. The sway is invisible beyond 100m but the shader runs at full cost regardless. Future optimisation: disable sway on distant chunks.
|
||||
|
||||
## The Target: RTX 3060
|
||||
|
||||
Our early access target is an RTX 3060 with 8GB VRAM. The rule is simple:
|
||||
Our early access target is an RTX 3060 with 8GB VRAM:
|
||||
|
||||
- If main island + full vegetation < 4GB VRAM --- ship it, we have 4GB headroom
|
||||
- If approaching 6--8GB --- implement lazy terrain nodes + cache eviction
|
||||
- If exceeding 8GB --- implement everything through vegetation LOD and region-level streaming
|
||||
- Main island + full vegetation < 4GB VRAM --- ship it, we have headroom
|
||||
- Approaching 6--8GB --- implement lazy terrain nodes + cache eviction
|
||||
- Exceeding 8GB --- implement vegetation LOD and region-level streaming
|
||||
|
||||
**Always measure before optimising.** We added VRAM logging before writing a single line of optimisation code. Half the "problems" we expected turned out to be non-issues. The other half were worse than expected. Profiling isn't optional.
|
||||
|
||||
@@ -121,4 +113,4 @@ Our early access target is an RTX 3060 with 8GB VRAM. The rule is simple:
|
||||
|
||||
Godot 4 can handle open worlds at this scale, but it won't do it for you. You need to build streaming, manage your own caches, audit your resource loading, and be disciplined about what runs per frame. The engine gives you the primitives --- MultiMesh, `LoadThreadedRequest`, `QueueFree` --- and it's up to you to wire them into a system that scales.
|
||||
|
||||
We're building Ariki with these systems and shipping to early access. If you're building something large-scale in Godot, we hope this is useful.
|
||||
We're building with these systems and developing the game using [Tinqs Studio](https://tinqs.com). If you're building something large-scale in Godot, we hope this is useful.
|
||||
|
||||
+27
-44
@@ -2,92 +2,75 @@
|
||||
title: "One Binary to Rule Them All: Building a Studio CLI"
|
||||
slug: studio-cli
|
||||
date: "2026-05-18"
|
||||
description: "We built tinqs-cli --- a single Go binary that handles machine identity, screenshots, cloud vision, health checks, and agent coordination across every machine in the studio."
|
||||
og_description: "A single Go binary for machine identity, screenshots, cloud vision, and agent coordination."
|
||||
description: "A single Go binary that handles machine identity, screenshots, cloud vision, and health checks. The glue that makes AI agents useful across a multi-machine game studio."
|
||||
og_description: "A single Go binary for machine identity, screenshots, cloud vision, and AI agent coordination."
|
||||
og_image: "https://www.tinqs.com/img/og-cover.jpg"
|
||||
excerpt: "tinqs-cli is a single Go binary that handles machine identity, screenshots, cloud vision, health checks, and agent coordination. One install, every machine, human or AI."
|
||||
excerpt: "A single Go binary that gives AI agents context about who you are, what machine you're on, and what services are reachable. Screenshots, cloud vision, health checks --- one install, every machine."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
Every machine in our studio runs the same Go binary. It knows who you are, what machine you're on, and what services are reachable. It takes screenshots, sends them to cloud vision, runs health checks, and coordinates AI agents. This is how we built tinqs-cli.
|
||||
Every machine in our studio runs the same Go binary. It knows who you are, what machine you're on, and what services are reachable. It takes screenshots, sends them to cloud vision, and runs health checks. This is the glue that makes AI agents actually useful in a multi-machine game studio.
|
||||
|
||||
## Why Build a CLI
|
||||
|
||||
When you have 9 machines across 5 people, two operating systems, and AI agents that need context about the environment they're running in, the glue becomes the hardest part. Which machine is this? What services are reachable? Is the game running? Can I take a screenshot of what the developer is looking at?
|
||||
When you have multiple machines across several people, two operating systems, and AI agents that need context about the environment they're running in, the glue becomes the hardest part. Which machine is this? What services are reachable? Is the game running? Can I take a screenshot of what the developer is looking at?
|
||||
|
||||
We tried shell scripts. We had a `setup.sh` for Mac, a `setup.ps1` for Windows, a `check-services.sh` for health, and a `screenshot.py` that never worked on Windows. They drifted. They broke. Nobody updated them.
|
||||
We tried shell scripts. A `setup.sh` for Mac, a `setup.ps1` for Windows, a `check-services.sh` for health checks, a `screenshot.py` that never worked on Windows. They drifted. They broke. Nobody updated them.
|
||||
|
||||
So we built one Go binary that does everything.
|
||||
|
||||
## The Identity System
|
||||
|
||||
The most important command is `tinqs-cli identity`. When an AI agent starts a new session --- Cursor, Claude Code, any tool --- the first thing it does is call this command. The output tells the agent:
|
||||
The most important command is `identity`. When an AI agent starts a new session --- Cursor, Claude Code, any tool --- the first thing it does is call this command. The output tells the agent:
|
||||
|
||||
- **Who you are.** The SOUL --- the agent's persistent identity, values, and operating principles.
|
||||
- **What company this is.** Team members, roles, contact info.
|
||||
- **What machine you're on.** Hostname, OS, which repos are cloned, what services are running.
|
||||
- **What siblings exist.** Other repos in the ecosystem and their purpose.
|
||||
- **What URLs are live.** Git platform, game server, bot, gateway --- with reachability status.
|
||||
- **The soul file** --- the agent's persistent identity, values, and operating principles
|
||||
- **Company context** --- team members, roles, what the company does
|
||||
- **Machine context** --- hostname, OS, which repos are cloned, what services are running
|
||||
- **Ecosystem** --- other repos and their purpose
|
||||
- **Service status** --- which URLs are live and reachable
|
||||
|
||||
This solves a fundamental problem with AI agents: **cold starts.** Every new chat window, every new agent tab, every new session is a blank slate. The agent doesn't know what project this is, who's asking, or what infrastructure exists. `tinqs-cli identity` gives it full context in one call.
|
||||
This solves a fundamental problem with AI agents: **cold starts.** Every new chat window, every new agent tab, every new session is a blank slate. The agent doesn't know what project this is, who's asking, or what infrastructure exists. One CLI call gives it full context.
|
||||
|
||||
The data lives in markdown files in the docs repo. The CLI reads them over the network via a private Tailscale mesh --- the docs repo is the source of truth, and any machine on the mesh can read it.
|
||||
The data lives in markdown files in the docs repo --- the source of truth. Any machine on the network can read it.
|
||||
|
||||
## Screenshots and Vision
|
||||
|
||||
`tinqs-cli screenshot --window "Ariki"` captures the game window from outside the process. No in-game overlay, no rendering pipeline integration. It uses the OS-level window capture API --- works on Windows (via GDI+) and Mac (via screencapture).
|
||||
The CLI can capture any window from outside the process. No in-game overlay, no rendering pipeline integration. It uses the OS-level window capture API --- works on Windows (via GDI+) and Mac (via screencapture).
|
||||
|
||||
`tinqs-cli photo --window "Ariki"` does the same thing but sends the screenshot to Amazon Bedrock's Nova Lite model for analysis. The agent says "take a photo of the game" and gets back a description of what's on screen: "The player character is standing near a half-built hut. There are 3 palm trees to the left. The terrain has a visible seam between two biomes."
|
||||
A `photo` command does the same thing but sends the screenshot to a cloud vision model for analysis. The agent says "take a photo of the game" and gets back a structured description: "The player character is standing near a half-built hut. There are 3 palm trees to the left. The terrain has a visible seam between two biomes."
|
||||
|
||||
This is how our CTO files bugs without typing. He looks at the game, tells the agent what's wrong, and the agent takes a screenshot, describes what it sees, and creates an issue with both the description and the image attached.
|
||||
This is how you file bugs without typing. Look at the game, tell the agent what's wrong, and the agent takes a screenshot, describes what it sees, and creates an issue with both the description and the image attached.
|
||||
|
||||
## Health Checks
|
||||
|
||||
`tinqs-cli doctor` runs a comprehensive health check across the studio:
|
||||
A `doctor` command runs a comprehensive health check:
|
||||
|
||||
- Is Tailscale connected? (Required for all inter-machine communication)
|
||||
- Is the git platform reachable? Can we authenticate?
|
||||
- Is the game simulation server running?
|
||||
- Is the bot service responding?
|
||||
- Is the game server running?
|
||||
- Are all expected repos cloned and on the right branch?
|
||||
- Is the Go version correct? Is Node.js installed?
|
||||
- Are required tools installed and at the right version?
|
||||
|
||||
The output is a green/yellow/red table. If something's wrong, the agent knows immediately and can diagnose or escalate.
|
||||
|
||||
## Cross-Machine Coordination
|
||||
|
||||
The studio has machines in London, and a server in AWS eu-west-1. They're connected via a Tailscale mesh network. tinqs-cli uses this mesh for everything --- reading identity files, checking service health, even routing agent commands.
|
||||
|
||||
When the CTO is on the Windows machine (Forge) and needs to check something on the Mac (Kraken), the agent doesn't SSH. It uses tinqs-cli to query the relevant service over Tailscale. The mesh is flat --- every machine can reach every other machine by hostname.
|
||||
|
||||
## Installation
|
||||
|
||||
One command per platform:
|
||||
|
||||
- **Windows:** `irm https://bot.arikigame.com/cli/install.ps1 | iex`
|
||||
- **Mac/Linux:** `curl -fsSL https://bot.arikigame.com/cli/install.sh | sh`
|
||||
|
||||
The install script downloads the latest binary from S3, places it in the PATH, and verifies the checksum. Updates are the same command --- idempotent, no package manager required.
|
||||
The output is a green/yellow/red table. If something's wrong, the agent knows immediately and can diagnose or escalate. This is essential for unattended agent sessions --- the agent can verify its environment before starting work.
|
||||
|
||||
## Why Go
|
||||
|
||||
Go compiles to a single static binary with no runtime dependencies. No Python virtualenvs, no Node.js version managers, no DLL hell on Windows. The same binary runs on the CTO's gaming PC, the designer's MacBook, and the CI runner in AWS.
|
||||
Go compiles to a single static binary with no runtime dependencies. No Python virtualenvs, no Node.js version managers, no DLL hell on Windows. The same binary runs on a gaming PC, a designer's MacBook, and a CI runner in AWS.
|
||||
|
||||
Cross-compilation is trivial. We build Windows, Mac (arm64 + amd64), and Linux binaries from a single GitHub Actions workflow. The release process is: push a tag, CI builds all three, uploads to S3, done.
|
||||
Cross-compilation is trivial. We build Windows, Mac (arm64 + amd64), and Linux binaries from a single CI workflow. Push a tag, CI builds all three, uploads to S3, done.
|
||||
|
||||
The binary is 15MB. It starts in under 100ms. It has zero runtime dependencies. For a tool that AI agents call on every session start, speed matters.
|
||||
|
||||
## What We Learned
|
||||
|
||||
**The CLI is the API for AI agents.** When we started, tinqs-cli was a convenience tool for humans. It became the primary interface for AI agents. The `identity` command was originally "nice to have" --- now it's the single most important function in our stack. Every agent session starts with it.
|
||||
**The CLI is the API for AI agents.** When we started, this was a convenience tool for humans. It became the primary interface for AI agents. The `identity` command was originally "nice to have" --- now it's the single most important function in our stack. Every agent session starts with it.
|
||||
|
||||
**One binary beats ten scripts.** Scripts rot. They have different shells, different PATH assumptions, different error handling. A compiled binary either works or it doesn't. It ships with its dependencies baked in. It doesn't care if your Python is 3.9 or 3.12.
|
||||
|
||||
**Tailscale makes networking disappear.** We spent zero time on VPN configuration, port forwarding, or firewall rules. Install Tailscale, join the mesh, done. Every machine is addressable by hostname. The CLI doesn't need to know about IPs, DNS, or network topology.
|
||||
|
||||
**Cloud vision is underrated for game dev.** Sending a screenshot to a vision model and getting back a structured description sounds gimmicky. In practice, it's the fastest way to document visual bugs. "The tree is floating 2m above the terrain" is much faster to write when the AI is looking at the same screen you are.
|
||||
|
||||
**Agent cold starts are the real problem.** Without the identity system, every new session starts with the agent asking "what project is this?" and the human re-explaining context. With it, the agent knows everything in 100ms. That's the difference between an AI assistant and an AI team member.
|
||||
|
||||
---
|
||||
|
||||
tinqs-cli is at v0.3.1 and growing. Every time we find ourselves writing a script that needs to work on multiple machines, we add a subcommand instead. The goal is simple: one binary that makes the studio work, whether the operator is human or AI.
|
||||
The CLI is part of [Tinqs Studio](https://tinqs.com) --- our game development platform that brings git hosting, AI agent tools, and team workflows together. Every time we find ourselves writing a script that needs to work on multiple machines, we add a subcommand instead. One binary that makes the studio work, whether the operator is human or AI.
|
||||
|
||||
Reference in New Issue
Block a user