Godot Development
Integrate Playcademy into your Godot projects
Overview
Integrate your Godot projects with the Playcademy platform using our Godot toolchain.
Installation
Install the Playcademy asset bundle from the Godot AssetLib and configure your project.
Install from AssetLib
- Open AssetLib tab in Godot
- Search for "Playcademy"
- Install the bundle
This adds addons/playcademy/ to your project.
Enable Plugins
Go to Project > Project Settings > Plugins and enable:
- Playcademy Backend (for local development servers)
- Playcademy Manifest Exporter (for deployment)
Setup AutoLoad
- Go to
Project > Project Settings > Globals > Autoload - Click the folder icon and navigate to
res://addons/playcademy/sdk/playcademy_sdk.gd - Name it
PlaycademySdk(this is the name you'll use in your code) - Click
+ Add
Need a Backend?
Godot's ecosystem doesn't offer a wide range of backend solutions like traditional app development.
Playcademy fills this gap with production-ready server infrastructure.
Backend Integrations
You can opt into server-side functionality using the Playcademy CLI:
Initialize Project
Run the init command in your Godot project directory:
$ playcademy init # Select integrations when promptedThis creates a playcademy.config.json with your project metadata.
Add Integrations
Add integrations during project setup or add them later:
$ playcademy timeback init # Educational tracking$ playcademy db init # SQLite database$ playcademy auth init # User accounts$ playcademy kv init # Key-value storage$ playcademy api init # Custom routesPlaycademy Backend Plugin
Upon starting, the Playcademy Backend plugin reads your playcademy.config.json.
It then launches the backend server with configured integrations.

Pre-configured Project
The fastest way to start a new Godot project with Playcademy:
bun create playcademy my-godot-appnpm create playcademy my-godot-apppnpm create playcademy my-godot-appyarn create playcademy my-godot-appWhen prompted, select Godot as your project type.
This scaffolds a project with:
- Playcademy addon pre-installed in
addons/playcademy/ - Sample scenes and scripts demonstrating fundamentals
playcademy.config.jsonconfigured for your project
Open the project folder in Godot and run Main.tscn to see the SDK in action.
Adding to Existing Project?
If you already have a Godot project, use playcademy init instead:
$ playcademy initThis adds the config file but won't install the addon automatically. See the Installation section for manual addon setup.
Local Development
The Playcademy Backend plugin automatically starts local development servers when you open your project in Godot.
- Sandbox Server: Simulates the Playcademy Platform API with mock data
- Backend Server: Runs your custom routes and integrations (call via
PlaycademySdk.backend)
Just Like the Vite Plugin
The Playcademy Backend plugin does for Godot what @playcademy/vite-plugin does for Vite projects.
In other words, it automatically manages local development infrastructure.
Project Settings
Configure the Playcademy Backend plugin via Godot's Project Settings.
- Go to
Project → Project Settings - Enable Advanced Settings (toggle in top-right)
- Find the
Playcademysection in the left sidebar
Sandbox
| Setting | Type | Default | Description |
|---|---|---|---|
playcademy/sandbox/auto_start | bool | false | Automatically start sandbox server on project open |
playcademy/sandbox/port | int | 4321 | Port for the sandbox server |
playcademy/sandbox/verbose | bool | false | Enable verbose logging |
playcademy/sandbox/url | String | http://localhost:4321 | URL of the sandbox server |
Backend
| Setting | Type | Default | Description |
|---|---|---|---|
playcademy/backend/auto_start | bool | false | Automatically start backend server on project open |
playcademy/backend/port | int | 8788 | Port for the backend server |
playcademy/backend/project_path | String | Auto | Path to project root (auto-detected from res://) |
Timeback
| Setting | Type | Default | Description |
|---|---|---|---|
playcademy/timeback/student_id | String | "" | Timeback student sourcedId (leave empty for auto-generated mock ID) |
playcademy/timeback/role | String | student | User role: student, parent, teacher, or administrator |
playcademy/timeback/organization_id | String | "" | Organization ID (leave empty for mock org) |
playcademy/timeback/organization_name | String | "" | Organization name (defaults to "Playcademy Studios") |
playcademy/timeback/organization_type | String | department | Organization type: school, district, department, local, state, or national |
Course Enrollment (Dynamic)
If you have a playcademy.config.json file with Timeback courses, the plugin automatically creates enrollment settings for each course:
| Setting | Type | Default | Description |
|---|---|---|---|
playcademy/timeback/courses/FastMath_3 | enum | Enrolled | Enroll mock user in FastMath Grade 3 |
playcademy/timeback/courses/FastMath_4 | enum | Enrolled | Enroll mock user in FastMath Grade 4 |
Set a course to "Not Enrolled" to test how your app behaves when a student is enrolled in specific grades only.
Requires JSON Config
Dynamic course settings only work with playcademy.config.json (not .js). GDScript can only parse JSON natively.
Testing Different Roles
Change the role setting to test how your app behaves for different Timeback user types during development.
For example, set role to parent to preview the parent experience, or teacher to test teacher-specific features.
Note: After changing Timeback settings, click Reset Database in the Playcademy dock to apply changes.
PlaycademySdk
This namespace is your main entry point for accessing the SDK in Godot projects.
Be sure to set it up as a global (autoload) in your settings for easy access throughout your project.
Initialization
Connect to SDK signals in your _ready() function:
func _ready():
if PlaycademySdk:
PlaycademySdk.sdk_ready.connect(_on_sdk_ready)
PlaycademySdk.sdk_initialization_failed.connect(_on_sdk_init_failed)
# Connect to API signals
PlaycademySdk.users.profile_received.connect(_on_profile_received)
PlaycademySdk.users.inventory_get_all_succeeded.connect(_on_inventory_received)
func _on_sdk_ready():
print("SDK Ready!")
PlaycademySdk.users.me()
PlaycademySdk.users.inventory_get_all()
func _on_sdk_init_failed(error: String):
printerr("SDK failed:", error)Core Namespaces
PlaycademySdk.users
User profile and inventory management:
# Get current user
PlaycademySdk.users.me()
func _on_profile_received(user_data: Dictionary):
print("User:", user_data.name)
print("ID:", user_data.id)Inventory:
# Get all inventory
PlaycademySdk.users.inventory_get_all()
func _on_inventory_get_all_succeeded(inventory: Array):
for item_entry in inventory:
var item = item_entry.get("item")
var quantity = item_entry.get("quantity")
print("Item:", item.name, "x", quantity)
# Add items
PlaycademySdk.users.inventory_add("sword-123", 1)
# Remove items
PlaycademySdk.users.inventory_remove("potion-456", 3)PlaycademySdk.credits
Manage platform currency:
# Get balance
PlaycademySdk.credits.balance()
func _on_balance_succeeded(balance: int):
print("Credits:", balance)
# Add credits
PlaycademySdk.credits.add(100)
# Spend credits
PlaycademySdk.credits.spend(50)PlaycademySdk.scores
Submit scores:
# Submit a score
PlaycademySdk.scores.submit(1500, { "level": 5, "difficulty": "hard" })
func _on_submit_succeeded(score_data: Dictionary):
print("Score submitted!")PlaycademySdk.runtime
Game lifecycle:
# Signal ready
PlaycademySdk.runtime.ready()
# Exit
PlaycademySdk.runtime.exit()Integration Namespaces
PlaycademySdk.timeback
Track learning activities with automatic XP calculation. Access user context for content gating.
# Start tracking an activity
var activity_metadata = {
"activityId": "math-quiz-1",
"grade": 3,
"subject": "Math"
}
PlaycademySdk.timeback.start_activity(activity_metadata)
# ... student completes the activity ...
# End activity and submit results (XP calculated automatically)
var score_data = {
"correctQuestions": 8,
"totalQuestions": 10
}
PlaycademySdk.timeback.end_activity(score_data)Auto-filled Metadata
The SDK automatically fills in metadata from your project config:
activityName: Derived fromactivityId(math-quiz-1→Math Quiz 1)appName: Fromplaycademy.config.json'snamefieldsensorUrl: Your deployed project URL
You can override any of these by providing them explicitly in start_activity().
User Context
Access the user's Timeback context via PlaycademySdk.timeback.user:
# Access user properties
var user = PlaycademySdk.timeback.user
var id = user.id # TimeBack user ID
var role = user.role # "student", "parent", "teacher", etc.
var enrollments = user.enrollments # Array of { subject, grade, courseId }
var orgs = user.organizations # Array of { id, name, type }
# Fetch fresh Timeback user data
PlaycademySdk.timeback.user.fetch()
# Fetch XP data
PlaycademySdk.timeback.user.xp.fetch({"include": ["today"]})user.id
The user's unique Timeback identifier:
var id = PlaycademySdk.timeback.user.id
# "abc123-def456-..."user.role
The user's primary Timeback role:
var role = PlaycademySdk.timeback.user.role
# "student", "parent", "teacher", "administrator", or "guardian"user.enrollments
Array of courses the user is enrolled in, scoped to your project:
var enrollments = PlaycademySdk.timeback.user.enrollments
# [{ "subject": "FastMath", "grade": 3, "courseId": "..." }, ...]App-Scoped
Enrollments are filtered to courses defined in your playcademy.config.json.
user.organizations
Array of all organizations (schools/districts) the user is affiliated with:
var orgs = PlaycademySdk.timeback.user.organizations
# [{ "id": "...", "name": "Playcademy Studios", "type": "school" }, ...]App-Scoped
Like enrollments, organizations are app-scoped.
Only organizations associated with the user's enrollments for your project are included.
user.fetch()
Fetch fresh user data from the server:
PlaycademySdk.timeback.user.fetch()Emits user_fetch_succeeded or user_fetch_failed signals. See Timeback Signals.
When to Fetch
The user context is initialized when the SDK loads.
Use fetch() if you need the latest data (e.g. after a user might have been enrolled in a new course mid-session).
user.xp
Access the user's XP data via PlaycademySdk.timeback.user.xp:
# Fetch XP data
PlaycademySdk.timeback.user.xp.fetch({
"include": ["today"], # Include today's XP breakdown
"force": false # Use cached data if available
})user.xp.fetch()
Fetch XP data from the server:
# Basic fetch (uses 5-second cache)
PlaycademySdk.timeback.user.xp.fetch()
# Include today's XP in response
PlaycademySdk.timeback.user.xp.fetch({
"include": ["today"]
})
# Filter by grade and subject (must provide both)
PlaycademySdk.timeback.user.xp.fetch({
"grade": 3,
"subject": "Math",
"include": ["today", "perCourse"]
})
# Bypass cache for fresh data
PlaycademySdk.timeback.user.xp.fetch({
"force": true
})Emits xp_fetch_succeeded or xp_fetch_failed signals. See Timeback Signals.
Options:
| Option | Type | Description | Default |
|---|---|---|---|
grade | int | Filter by grade level (requires subject) | - |
subject | String | Filter by subject (requires grade) | - |
include | Array | Extra data: "today", "perCourse" | [] |
force | bool | Bypass 5-second cache | false |
Response Dictionary:
| Field | Type | Description | Always Present |
|---|---|---|---|
totalXp | int | Total XP earned | Yes |
todayXp | int | XP earned today (if "today" in include) | No |
courses | Array | Per-course breakdown (if "perCourse" in include) | No |
Each course in courses array:
| Field | Type | Description |
|---|---|---|
grade | int | Course grade level |
subject | String | Course subject |
title | String | Course display name |
totalXp | int | XP earned in course |
todayXp | int | Today's XP (if included) |
Caching
XP data is cached for 5 seconds to prevent excessive API calls. Use "force": true when you need guaranteed fresh data (e.g., after completing an activity).
start_activity
Begin tracking a learning activity. Starts an internal timer and sets up metadata for OneRoster submission.
# Minimal (most common)
var activity_metadata = {
"activityId": "math-quiz-1",
"grade": 3,
"subject": "Math"
}
# Activity name auto-derived: "Math Quiz 1"
PlaycademySdk.timeback.start_activity(activity_metadata)
# With custom name override
var custom_activity = {
"activityId": "multiplication-drill",
"activityName": "Advanced Multiplication Drill",
"grade": 4,
"subject": "Math"
}
PlaycademySdk.timeback.start_activity(custom_activity)Required Fields:
| Field | Type | Description | Example |
|---|---|---|---|
activityId | String | Identifier for this activity | "math-quiz-1" |
grade | int | Grade level | 3 |
subject | String | Subject area | "Math" |
Optional Fields:
| Field | Type | Description | Default |
|---|---|---|---|
activityName | String | Display name for the activity | Prettified activityId |
appName | String | Application name | From playcademy.config.json |
sensorUrl | String | URL where activity is hosted | Deployed project URL |
Course Routing
The grade and subject fields determine which OneRoster course receives the activity data.
Ensure these match a course in your playcademy.config.json Timeback configuration.
end_activity
End the current activity and submit results to OneRoster. Calculates XP based on accuracy and active time.
# Auto-calculate XP based on score
var score_data = {
"correctQuestions": 8,
"totalQuestions": 10
}
PlaycademySdk.timeback.end_activity(score_data)
# Report mastery (e.g., unit completed)
var score_with_mastery = {
"correctQuestions": 8,
"totalQuestions": 10,
"masteredUnits": 1 # Student mastered 1 unit
}
PlaycademySdk.timeback.end_activity(score_with_mastery)
# Override XP calculation
var score_with_custom_xp = {
"correctQuestions": 8,
"totalQuestions": 10,
"xpAwarded": 15 # Award exactly 15 XP
}
PlaycademySdk.timeback.end_activity(score_with_custom_xp)Required Fields:
| Field | Type | Description | Example |
|---|---|---|---|
correctQuestions | int | Number of correct answers | 8 |
totalQuestions | int | Total number of questions | 10 |
Optional Fields:
| Field | Type | Description | Default |
|---|---|---|---|
xpAwarded | int | Override automatic XP calculation | Based on time and accuracy |
masteredUnits | int | Number of units mastered | 0 |
When to Report Mastery
Send masteredUnits: 1 when the student completes a discrete learning unit in your app:
- Level-based: Student completes a level, stage, or world
- Rank-based: Student earns a rank, tier, or badge
- Skills-based: Student masters a skill, competency, or standard
- Module-based: Student completes a module, quiz, or chapter
The platform tracks cumulative mastery and calculates completion automatically based on your mastery configuration.
XP Calculation
By default, XP is calculated as:
Base XP = Active time in minutes × Accuracy multiplier
| Accuracy | Multiplier | Example (10 min) |
|---|---|---|
| 100% | 1.25× | 10 min × 1.25 = 12.5 XP |
| 80-99% | 1.0× | 10 min × 1.0 = 10 XP |
| < 80% | 0× | 0 XP (mastery not demonstrated) |
Base rate: 1 minute of active learning = 1 XP
Re-attempts earn diminishing XP: 50% on 1st re-attempt, 25% on 2nd, 0% on 3rd+.
pause_activity
Pause the activity timer. Use this during non-instructional moments like showing feedback or explanations.
PlaycademySdk.timeback.start_activity({
"activityId": "speed-math-1",
"grade": 4,
"subject": "Math"
})
# Student attempts a problem...
func _on_answer_submitted(is_correct: bool):
if not is_correct:
# Pause timer during feedback
PlaycademySdk.timeback.pause_activity()
# Show correct answer or explanation
show_correct_answer()
await get_tree().create_timer(3.0).timeout
# Resume timer when they continue
PlaycademySdk.timeback.resume_activity()
# End activity (only active time counted)
PlaycademySdk.timeback.end_activity({
"correctQuestions": 40,
"totalQuestions": 50
})When to Pause
Pause when:
- Tutorial/instruction screens
- Showing hints or explanations
- Waiting for external resources to load
- Any non-active learning time
This ensures XP reflects actual learning time.
resume_activity
Resume the activity timer after a pause.
# After pausing
PlaycademySdk.timeback.pause_activity()
# ... show feedback ...
# Resume when ready
PlaycademySdk.timeback.resume_activity()Must Call start_activity First
You must call start_activity() before using pause_activity() or resume_activity().
Calling these methods without an active activity will trigger a failure signal.
Timeback Signals
Connect to signals to handle activity, user, and XP responses:
func _ready():
# Activity signals
PlaycademySdk.timeback.end_activity_succeeded.connect(_on_end_activity_succeeded)
PlaycademySdk.timeback.end_activity_failed.connect(_on_end_activity_failed)
PlaycademySdk.timeback.pause_activity_failed.connect(_on_pause_activity_failed)
PlaycademySdk.timeback.resume_activity_failed.connect(_on_resume_activity_failed)
# User fetch signals
PlaycademySdk.timeback.user_fetch_succeeded.connect(_on_user_fetch_succeeded)
PlaycademySdk.timeback.user_fetch_failed.connect(_on_user_fetch_failed)
# XP fetch signals
PlaycademySdk.timeback.xp_fetch_succeeded.connect(_on_xp_fetch_succeeded)
PlaycademySdk.timeback.xp_fetch_failed.connect(_on_xp_fetch_failed)
# Handle activity success
func _on_end_activity_succeeded(response):
print("Activity ended! XP awarded:", response.xpAwarded)
# Handle activity failures
func _on_end_activity_failed(error: String):
printerr("Failed to end activity:", error)
func _on_pause_activity_failed(error: String):
printerr("Failed to pause activity:", error)
func _on_resume_activity_failed(error: String):
printerr("Failed to resume activity:", error)
# Handle user fetch
func _on_user_fetch_succeeded(user_data: Dictionary):
print("User data refreshed:", user_data)
func _on_user_fetch_failed(error: String):
printerr("Failed to fetch user data:", error)
# Handle XP fetch
func _on_xp_fetch_succeeded(xp_data: Dictionary):
print("Total XP:", xp_data.get("totalXp", 0))
if xp_data.has("todayXp"):
print("Today's XP:", xp_data.todayXp)
func _on_xp_fetch_failed(error: String):
printerr("Failed to fetch XP data:", error)Available Signals:
| Signal | When | Payload |
|---|---|---|
end_activity_succeeded | Activity ended successfully | Dictionary |
end_activity_failed | Failed to end activity | String |
pause_activity_failed | Failed to pause activity | String |
resume_activity_failed | Failed to resume activity | String |
user_fetch_succeeded | User data fetched | Dictionary |
user_fetch_failed | Failed to fetch user data | String |
xp_fetch_succeeded | XP data fetched | Dictionary |
xp_fetch_failed | Failed to fetch XP data | String |
PlaycademySdk.backend
Call your custom backend API routes.
These methods connect to the server-side routes you create in your server/api/ directory.
Learn more about custom routes.
request
Make HTTP requests to your custom backend routes.
# Connect signal
func _ready():
PlaycademySdk.backend.request_succeeded.connect(_on_data_received)
# Make GET request
func fetch_player_stats():
PlaycademySdk.backend.request("/stats", "GET")
# Handle response
func _on_data_received(response: Dictionary):
print("Player stats:", response)
var score = response.get("score", 0)
var level = response.get("level", 1)
update_ui(score, level)# Connect signal
func _ready():
PlaycademySdk.backend.request_succeeded.connect(_on_answer_validated)
# Submit answer
func submit_answer(answer: String):
var body = {
"answer": answer,
"questionId": current_question_id,
"timestamp": Time.get_unix_time_from_system()
}
PlaycademySdk.backend.request("/validate", "POST", body)
# Handle validation result
func _on_answer_validated(response: Dictionary):
var is_correct = response.get("correct", false)
if is_correct:
show_correct_feedback()
else:
show_incorrect_feedback()# Connect signal
func _ready():
PlaycademySdk.backend.request_succeeded.connect(_on_backend_success)
# Update user settings
func update_settings(volume: float, difficulty: String):
var settings = {
"volume": volume,
"difficulty": difficulty,
"lastUpdated": Time.get_unix_time_from_system()
}
PlaycademySdk.backend.request("/settings", "PUT", settings)
# Clear cache
func clear_cache():
PlaycademySdk.backend.request("/cache/clear", "DELETE")
# Handle success
func _on_backend_success(response: Dictionary):
print("Operation completed:", response)# Connect both signals
func _ready():
PlaycademySdk.backend.request_succeeded.connect(_on_backend_success)
PlaycademySdk.backend.request_failed.connect(_on_backend_error)
# Make request with error handling
func save_app_state(app_data: Dictionary):
PlaycademySdk.backend.request("/save", "POST", app_data)
show_loading_indicator()
# Handle success
func _on_backend_success(response: Dictionary):
hide_loading_indicator()
show_notification("Progress saved!")
print("Save successful:", response)
# Handle errors
func _on_backend_error(error: String):
hide_loading_indicator()
show_notification("Failed to save. Please try again.")
printerr("Backend error:", error)
# Maybe retry or fallback to local save
save_to_local_storage(app_data)Use per-request callbacks when you want a response handler scoped to a single call.
func fetch_player_stats():
PlaycademySdk.backend.request(
"/stats",
"GET",
{},
_on_stats_received,
_on_stats_failed
)
func _on_stats_received(response: Dictionary):
print("Player stats:", response)
func _on_stats_failed(error: String):
printerr("Failed to fetch stats:", error)If you are using per-request callbacks and you also have global backend listeners
func fetch_player_stats_quiet():
PlaycademySdk.backend.request(
"/stats",
"GET",
{},
_on_stats_received,
_on_stats_failed,
true # suppress_signals
)Callbacks + Signals
By default, callbacks are additive: if you pass on_success / on_failure,
the SDK will still emit request_succeeded / request_failed unless you pass
true for the sixth positional argument (suppress_signals).
Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
path | String | API route path | "/hello" |
method | String | HTTP method (GET, POST, PUT, DELETE) | "POST" |
body | Dictionary | Request body (optional) | { "answer": "A" } |
on_success | Callable | Called on success (optional) | _on_stats_received |
on_failure | Callable | Called on failure (optional) | _on_stats_failed |
suppress_signals | bool | If true, do not emit request_succeeded / request_failed for this request | true |
Supported Methods:
GET- Retrieve dataPOST- Create or submit dataPUT- Update dataPATCH- Partial updateDELETE- Remove data
Backend Signals
Connect to signals to handle backend responses:
func _ready():
PlaycademySdk.backend.request_succeeded.connect(_on_backend_succeeded)
PlaycademySdk.backend.request_failed.connect(_on_backend_failed)
func _on_backend_succeeded(response: Dictionary):
print("Backend response:", response)
# Access response data
if response.has("message"):
print("Message:", response.message)
func _on_backend_failed(error: String):
printerr("Backend request failed:", error)Available Signals:
| Signal | Description | Payload |
|---|---|---|
request_succeeded | Request completed | Dictionary |
request_failed | Request failed or timed out | String |
Custom Routes
To create backend routes, add files to your server/api/ directory:
export default defineRoute({
GET: async (req, ctx) => {
return { message: 'Hello from backend!' }
},
})See Custom Routes for complete documentation.
Export and Deploy
The CLI can automatically export and deploy your Godot project.
Configure Web Export
Create a Web export preset in Godot:
- Go to
Project → Export... - Add
Web (Runnable)preset - Set
Custom HTML Shellto:res://addons/playcademy/shell.html
Deploy
Run the deploy command:
$ playcademy deployThe CLI will:
- Detect your Godot project automatically
- Prompt: "Export Godot project?"
- Run headless export to generate build files
- Deploy to Playcademy
Automatic Export
The CLI finds your Godot executable and runs the export headlessly; no need to manually export from the editor!
See CLI Deployment for complete deployment documentation.
Signal Reference
All SDK methods emit signals for responses. Some APIs (like backend.request) also support per-request callbacks as an alternative to global listeners.
Users API:
| Signal | When |
|---|---|
profile_received | User data loaded |
profile_fetch_failed | Failed to load user |
inventory_get_all_succeeded | Inventory loaded |
inventory_get_all_failed | Inventory error |
inventory_add_succeeded | Item added |
inventory_add_failed | Add item error |
inventory_remove_succeeded | Item removed |
inventory_remove_failed | Remove item error |
inventory_changed | Any inventory change |
Credits API:
| Signal | When |
|---|---|
balance_succeeded | Balance retrieved |
balance_failed | Balance error |
add_succeeded | Credits added |
add_failed | Add credits error |
spend_succeeded | Credits spent |
spend_failed | Spend error |
Scores API:
| Signal | When |
|---|---|
submit_succeeded | Score submitted |
submit_failed | Submit error |
get_by_user_succeeded | Scores retrieved |
get_by_user_failed | Retrieve error |
Integration Signals
For Timeback and Backend API signals, see their respective sections:
What's Next?
Custom Routes
Add server-side logic and backend APIs to your Godot project.
Timeback Integration
Set up educational tracking and XP rewards for learning activities.
Deployment Guide
Master the deployment workflow for Godot projects.
Web Development Guide
Learn about web-specific features and best practices.
