PlaycademyPlaycademy

Godot Development

Integrate Playcademy into your Godot projects

Overview

Integrate your Godot projects with the Playcademy platform using our Godot toolchain.

Godot integration architecture showing setup and runtime phases

Installation

Install the Playcademy asset bundle from the Godot AssetLib and configure your project.

Install from AssetLib

  1. Open AssetLib tab in Godot
  2. Search for "Playcademy"
  3. 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

  1. Go to Project > Project Settings > Globals > Autoload
  2. Click the folder icon and navigate to res://addons/playcademy/sdk/playcademy_sdk.gd
  3. Name it PlaycademySdk (this is the name you'll use in your code)
  4. Click + Add
Watch the complete setup process from installing the asset bundle to configuring plugins

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

TimebackLearn More →DatabaseLearn More →AuthenticationLearn More →KV StorageLearn More →Bucket StorageLearn More →Custom RoutesLearn More →

You can opt into server-side functionality using the Playcademy CLI:

Initialize Project

Run the init command in your Godot project directory:

Command
$ playcademy init # Select integrations when prompted

This creates a playcademy.config.json with your project metadata.

Add Integrations

Add integrations during project setup or add them later:

Command
$ 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 routes

Playcademy Backend Plugin

Upon starting, the Playcademy Backend plugin reads your playcademy.config.json.

It then launches the backend server with configured integrations.

Playcademy Backend Plugin in Godot


Pre-configured Project

The fastest way to start a new Godot project with Playcademy:

bun create playcademy my-godot-app
npm create playcademy my-godot-app
pnpm create playcademy my-godot-app
yarn create playcademy my-godot-app

When 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.json configured 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:

Command
$ playcademy init

This 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.

  1. Sandbox Server: Simulates the Playcademy Platform API with mock data
  2. 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.

  1. Go to Project → Project Settings
  2. Enable Advanced Settings (toggle in top-right)
  3. Find the Playcademy section in the left sidebar

Sandbox

SettingTypeDefaultDescription
playcademy/sandbox/auto_startboolfalseAutomatically start sandbox server on project open
playcademy/sandbox/portint4321Port for the sandbox server
playcademy/sandbox/verboseboolfalseEnable verbose logging
playcademy/sandbox/urlStringhttp://localhost:4321URL of the sandbox server

Backend

SettingTypeDefaultDescription
playcademy/backend/auto_startboolfalseAutomatically start backend server on project open
playcademy/backend/portint8788Port for the backend server
playcademy/backend/project_pathStringAutoPath to project root (auto-detected from res://)

Timeback

SettingTypeDefaultDescription
playcademy/timeback/student_idString""Timeback student sourcedId (leave empty for auto-generated mock ID)
playcademy/timeback/roleStringstudentUser role: student, parent, teacher, or administrator
playcademy/timeback/organization_idString""Organization ID (leave empty for mock org)
playcademy/timeback/organization_nameString""Organization name (defaults to "Playcademy Studios")
playcademy/timeback/organization_typeStringdepartmentOrganization 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:

SettingTypeDefaultDescription
playcademy/timeback/courses/FastMath_3enumEnrolledEnroll mock user in FastMath Grade 3
playcademy/timeback/courses/FastMath_4enumEnrolledEnroll 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.

Example
# 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 from activityId (math-quiz-1Math Quiz 1)
  • appName: From playcademy.config.json's name field
  • sensorUrl: 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:

Example
# 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:

OptionTypeDescriptionDefault
gradeintFilter by grade level (requires subject)-
subjectStringFilter by subject (requires grade)-
includeArrayExtra data: "today", "perCourse"[]
forceboolBypass 5-second cachefalse

Response Dictionary:

FieldTypeDescriptionAlways Present
totalXpintTotal XP earnedYes
todayXpintXP earned today (if "today" in include)No
coursesArrayPer-course breakdown (if "perCourse" in include)No

Each course in courses array:

FieldTypeDescription
gradeintCourse grade level
subjectStringCourse subject
titleStringCourse display name
totalXpintXP earned in course
todayXpintToday'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.

start_activity Example
# 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:

FieldTypeDescriptionExample
activityIdStringIdentifier for this activity"math-quiz-1"
gradeintGrade level3
subjectStringSubject area"Math"

Optional Fields:

FieldTypeDescriptionDefault
activityNameStringDisplay name for the activityPrettified activityId
appNameStringApplication nameFrom playcademy.config.json
sensorUrlStringURL where activity is hostedDeployed 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.

end_activity Example
# 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:

FieldTypeDescriptionExample
correctQuestionsintNumber of correct answers8
totalQuestionsintTotal number of questions10

Optional Fields:

FieldTypeDescriptionDefault
xpAwardedintOverride automatic XP calculationBased on time and accuracy
masteredUnitsintNumber of units mastered0

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

AccuracyMultiplierExample (10 min)
100%1.25×10 min × 1.25 = 12.5 XP
80-99%1.0×10 min × 1.0 = 10 XP
< 80%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.

pause_activity Example
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.

resume_activity Example
# 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:

SignalWhenPayload
end_activity_succeededActivity ended successfullyDictionary
end_activity_failedFailed to end activityString
pause_activity_failedFailed to pause activityString
resume_activity_failedFailed to resume activityString
user_fetch_succeededUser data fetchedDictionary
user_fetch_failedFailed to fetch user dataString
xp_fetch_succeededXP data fetchedDictionary
xp_fetch_failedFailed to fetch XP dataString

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:

ParameterTypeDescriptionExample
pathStringAPI route path"/hello"
methodStringHTTP method (GET, POST, PUT, DELETE)"POST"
bodyDictionaryRequest body (optional){ "answer": "A" }
on_successCallableCalled on success (optional)_on_stats_received
on_failureCallableCalled on failure (optional)_on_stats_failed
suppress_signalsboolIf true, do not emit request_succeeded / request_failed for this requesttrue

Supported Methods:

  • GET - Retrieve data
  • POST - Create or submit data
  • PUT - Update data
  • PATCH - Partial update
  • DELETE - Remove data

Backend Signals

Connect to signals to handle backend responses:

Backend Signal Handling
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:

SignalDescriptionPayload
request_succeededRequest completedDictionary
request_failedRequest failed or timed outString

Custom Routes

To create backend routes, add files to your server/api/ directory:

server/api/hello.ts
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:

  1. Go to Project → Export...
  2. Add Web (Runnable) preset
  3. Set Custom HTML Shell to: res://addons/playcademy/shell.html

Deploy

Run the deploy command:

Command
$ playcademy deploy

The CLI will:

  1. Detect your Godot project automatically
  2. Prompt: "Export Godot project?"
  3. Run headless export to generate build files
  4. 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:

SignalWhen
profile_receivedUser data loaded
profile_fetch_failedFailed to load user
inventory_get_all_succeededInventory loaded
inventory_get_all_failedInventory error
inventory_add_succeededItem added
inventory_add_failedAdd item error
inventory_remove_succeededItem removed
inventory_remove_failedRemove item error
inventory_changedAny inventory change

Credits API:

SignalWhen
balance_succeededBalance retrieved
balance_failedBalance error
add_succeededCredits added
add_failedAdd credits error
spend_succeededCredits spent
spend_failedSpend error

Scores API:

SignalWhen
submit_succeededScore submitted
submit_failedSubmit error
get_by_user_succeededScores retrieved
get_by_user_failedRetrieve error

Integration Signals

For Timeback and Backend API signals, see their respective sections:


What's Next?

On this page