new file: docs/EXPORTING_GAMES.md
This commit is contained in:
parent
4f4cc10a76
commit
d13c2cdfdc
11 changed files with 5190 additions and 58 deletions
|
|
@ -714,5 +714,5 @@ func _physics_process(delta):
|
||||||
|
|
||||||
- [Getting Started Guide](../GETTING_STARTED.md)
|
- [Getting Started Guide](../GETTING_STARTED.md)
|
||||||
- [Cloud Services Architecture](CLOUD_SERVICES_ARCHITECTURE.md)
|
- [Cloud Services Architecture](CLOUD_SERVICES_ARCHITECTURE.md)
|
||||||
- [Multiplayer Tutorial](tutorials/MULTIPLAYER_TUTORIAL.md)
|
- [Multiplayer Pong Tutorial](tutorials/FIRST_GAME_TUTORIAL.md)
|
||||||
- [Studio Bridge Guide](STUDIO_BRIDGE_GUIDE.md)
|
- [Studio Bridge Guide](STUDIO_BRIDGE_GUIDE.md)
|
||||||
|
|
|
||||||
896
docs/EXPORTING_GAMES.md
Normal file
896
docs/EXPORTING_GAMES.md
Normal file
|
|
@ -0,0 +1,896 @@
|
||||||
|
# Exporting Games with AeThex
|
||||||
|
|
||||||
|
This guide covers exporting your AeThex game to all supported platforms: Windows, Linux, macOS, Web (HTML5), and Android.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
AeThex supports exporting to:
|
||||||
|
- **Desktop:** Windows, Linux, macOS
|
||||||
|
- **Web:** HTML5 (WebAssembly + WebGL)
|
||||||
|
- **Mobile:** Android (iOS planned)
|
||||||
|
|
||||||
|
Each platform has specific requirements and optimization considerations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before Exporting
|
||||||
|
|
||||||
|
### 1. Test Your Game
|
||||||
|
|
||||||
|
Always test thoroughly before exporting:
|
||||||
|
```bash
|
||||||
|
# Run in editor
|
||||||
|
aethex --editor --path ./my-project
|
||||||
|
|
||||||
|
# Test in release mode
|
||||||
|
aethex --path ./my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Project Settings
|
||||||
|
|
||||||
|
**Project → Project Settings → Application:**
|
||||||
|
```
|
||||||
|
Name: Your Game Name
|
||||||
|
Description: Game description
|
||||||
|
Icon: res://icon.png
|
||||||
|
Version: 1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Display Settings:**
|
||||||
|
```
|
||||||
|
Window Width: 1920
|
||||||
|
Window Height: 1080
|
||||||
|
Fullscreen: false
|
||||||
|
Resizable: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Export Templates
|
||||||
|
|
||||||
|
Download export templates for your target platform:
|
||||||
|
```bash
|
||||||
|
# Via Studio IDE: Editor → Export Templates → Download
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
wget https://aethex.io/downloads/export-templates-[version].zip
|
||||||
|
unzip export-templates-[version].zip -d ~/.local/share/aethex/templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows Export
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Build Machine:** Windows, Linux, or macOS
|
||||||
|
- **Target:** Windows 7+ (64-bit)
|
||||||
|
- **Export Template:** Windows Desktop template
|
||||||
|
|
||||||
|
### Export Steps
|
||||||
|
|
||||||
|
**1. Add Export Preset:**
|
||||||
|
```
|
||||||
|
Project → Export
|
||||||
|
Click "Add..." → Windows Desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configure Options:**
|
||||||
|
```
|
||||||
|
Export Path: builds/windows/YourGame.exe
|
||||||
|
Architecture: x86_64
|
||||||
|
Runnable: ✓ (for executable)
|
||||||
|
Embed PCK: ✓ (single file)
|
||||||
|
|
||||||
|
Code Signing:
|
||||||
|
- Identity: (optional) Your signing certificate
|
||||||
|
- Password: Your certificate password
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Export:**
|
||||||
|
```
|
||||||
|
Click "Export PCK/ZIP" for package only
|
||||||
|
Click "Export Project" for executable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows-Specific Options
|
||||||
|
|
||||||
|
**Application:**
|
||||||
|
```
|
||||||
|
Company Name: Your Company
|
||||||
|
Product Name: Your Game
|
||||||
|
File Version: 1.0.0.0
|
||||||
|
Product Version: 1.0.0.0
|
||||||
|
File Description: Your game description
|
||||||
|
Copyright: © 2024 Your Company
|
||||||
|
Trademarks: Your trademarks
|
||||||
|
```
|
||||||
|
|
||||||
|
**Executable:**
|
||||||
|
```
|
||||||
|
Console Wrapper: ✗ (disable for release)
|
||||||
|
Icon: res://icon.ico (Windows icon format)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code Signing:**
|
||||||
|
```gdscript
|
||||||
|
# Sign your executable (optional but recommended)
|
||||||
|
signtool sign /f certificate.pfx /p password YourGame.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows Distribution
|
||||||
|
|
||||||
|
**Standalone:**
|
||||||
|
- Single `.exe` file
|
||||||
|
- Portable, no installation required
|
||||||
|
- Share via download link or USB
|
||||||
|
|
||||||
|
**Installer (Optional):**
|
||||||
|
```bash
|
||||||
|
# Use NSIS, Inno Setup, or WiX
|
||||||
|
# Example with NSIS:
|
||||||
|
makensis installer.nsi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steam:**
|
||||||
|
```bash
|
||||||
|
# Use Steamworks SDK
|
||||||
|
# Follow Steam's integration guide
|
||||||
|
# Configure depot builds
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux Export
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Target:** Ubuntu 20.04+, most modern distros
|
||||||
|
- **Architecture:** x86_64, ARM64
|
||||||
|
- **Export Template:** Linux/X11 template
|
||||||
|
|
||||||
|
### Export Steps
|
||||||
|
|
||||||
|
**1. Add Export Preset:**
|
||||||
|
```
|
||||||
|
Project → Export
|
||||||
|
Click "Add..." → Linux/X11
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configure Options:**
|
||||||
|
```
|
||||||
|
Export Path: builds/linux/YourGame.x86_64
|
||||||
|
Architecture: x86_64 (or arm64)
|
||||||
|
Runnable: ✓
|
||||||
|
Embed PCK: ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Export:**
|
||||||
|
```
|
||||||
|
Click "Export Project"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux-Specific Options
|
||||||
|
|
||||||
|
**Binary:**
|
||||||
|
```
|
||||||
|
Strip Debug Symbols: ✓ (reduces size)
|
||||||
|
Make Executable: ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
```bash
|
||||||
|
# Your game requires these libraries on user's system:
|
||||||
|
# - libGL.so.1
|
||||||
|
# - libX11.so.6
|
||||||
|
# - libXcursor.so.1
|
||||||
|
# - libXrandr.so.2
|
||||||
|
# - libXi.so.6
|
||||||
|
|
||||||
|
# Most modern distros include these
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux Distribution
|
||||||
|
|
||||||
|
**AppImage (Recommended):**
|
||||||
|
```bash
|
||||||
|
# Create portable AppImage
|
||||||
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||||
|
chmod +x appimagetool-x86_64.AppImage
|
||||||
|
|
||||||
|
# Structure:
|
||||||
|
# YourGame.AppDir/
|
||||||
|
# ├── AppRun (symlink to your binary)
|
||||||
|
# ├── YourGame.x86_64
|
||||||
|
# ├── YourGame.desktop
|
||||||
|
# └── icon.png
|
||||||
|
|
||||||
|
./appimagetool-x86_64.AppImage YourGame.AppDir
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flatpak:**
|
||||||
|
```bash
|
||||||
|
# Create Flatpak manifest
|
||||||
|
# Follow Flathub guidelines
|
||||||
|
flatpak-builder build-dir com.yourcompany.yourgame.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Snap:**
|
||||||
|
```bash
|
||||||
|
# Create snapcraft.yaml
|
||||||
|
snapcraft
|
||||||
|
```
|
||||||
|
|
||||||
|
**.tar.gz Archive:**
|
||||||
|
```bash
|
||||||
|
# Simple distribution
|
||||||
|
tar -czf YourGame-linux.tar.gz YourGame.x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## macOS Export
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Build Machine:** macOS (for signing/notarization)
|
||||||
|
- **Target:** macOS 10.13+
|
||||||
|
- **Export Template:** macOS template
|
||||||
|
- **Apple Developer Account:** For signing (required for distribution)
|
||||||
|
|
||||||
|
### Export Steps
|
||||||
|
|
||||||
|
**1. Add Export Preset:**
|
||||||
|
```
|
||||||
|
Project → Export
|
||||||
|
Click "Add..." → macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configure Options:**
|
||||||
|
```
|
||||||
|
Export Path: builds/macos/YourGame.zip
|
||||||
|
Architecture: universal (Intel + Apple Silicon)
|
||||||
|
or separate: x86_64, arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Code Signing:**
|
||||||
|
```
|
||||||
|
Codesign:
|
||||||
|
Identity: "Developer ID Application: Your Name (TEAM_ID)"
|
||||||
|
Certificate Path: /path/to/certificate.p12
|
||||||
|
Certificate Password: your_password
|
||||||
|
|
||||||
|
Entitlements: res://entitlements.plist (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Export:**
|
||||||
|
```
|
||||||
|
Click "Export Project"
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS-Specific Options
|
||||||
|
|
||||||
|
**Application Bundle:**
|
||||||
|
```
|
||||||
|
Bundle ID: com.yourcompany.yourgame
|
||||||
|
Display Name: Your Game
|
||||||
|
Version: 1.0.0
|
||||||
|
Copyright: © 2024 Your Company
|
||||||
|
Icon: res://icon.icns (macOS icon format)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hardened Runtime:**
|
||||||
|
```
|
||||||
|
Enable Hardened Runtime: ✓
|
||||||
|
Disable Library Validation: ✗
|
||||||
|
Allow JIT Code: ✗ (unless needed)
|
||||||
|
Allow Unsigned Executable Memory: ✗
|
||||||
|
Allow DYLD Environment Variables: ✗
|
||||||
|
Disable Executable Memory Protection: ✗
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notarization (Required for macOS 10.15+)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sign the app
|
||||||
|
codesign --deep --force --verify --verbose \
|
||||||
|
--sign "Developer ID Application: Your Name (TEAM_ID)" \
|
||||||
|
--options runtime \
|
||||||
|
YourGame.app
|
||||||
|
|
||||||
|
# Create ZIP for notarization
|
||||||
|
ditto -c -k --keepParent YourGame.app YourGame.zip
|
||||||
|
|
||||||
|
# Submit for notarization
|
||||||
|
xcrun notarytool submit YourGame.zip \
|
||||||
|
--apple-id your@email.com \
|
||||||
|
--team-id TEAM_ID \
|
||||||
|
--password app-specific-password \
|
||||||
|
--wait
|
||||||
|
|
||||||
|
# Staple notarization ticket
|
||||||
|
xcrun stapler staple YourGame.app
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
spctl -a -vv YourGame.app
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS Distribution
|
||||||
|
|
||||||
|
**Direct Download:**
|
||||||
|
- Distribute `.dmg` or `.zip`
|
||||||
|
- Users drag to Applications folder
|
||||||
|
|
||||||
|
**Mac App Store:**
|
||||||
|
- Use App Store Connect
|
||||||
|
- Follow Apple's submission guidelines
|
||||||
|
- Use "Mac App Store" export preset
|
||||||
|
|
||||||
|
**Create DMG:**
|
||||||
|
```bash
|
||||||
|
# Create disk image
|
||||||
|
hdiutil create -volname "Your Game" -srcfolder YourGame.app -ov -format UDZO YourGame.dmg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web (HTML5) Export
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Target:** Modern browsers (Chrome, Firefox, Safari, Edge)
|
||||||
|
- **Export Template:** Web template
|
||||||
|
- **Web Server:** For hosting
|
||||||
|
|
||||||
|
### Export Steps
|
||||||
|
|
||||||
|
**1. Add Export Preset:**
|
||||||
|
```
|
||||||
|
Project → Export
|
||||||
|
Click "Add..." → Web (HTML5)
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configure Options:**
|
||||||
|
```
|
||||||
|
Export Path: builds/web/index.html
|
||||||
|
Head Include: res://web/head.html (custom HTML)
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Export:**
|
||||||
|
```
|
||||||
|
Click "Export Project"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web-Specific Options
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
```
|
||||||
|
WebGL Version: 2.0 (WebGL 2)
|
||||||
|
Enable Run: ✓ (for testing)
|
||||||
|
Full Window Size: ✓
|
||||||
|
|
||||||
|
Memory Settings:
|
||||||
|
Initial Memory: 33554432 (32MB)
|
||||||
|
Max Memory: 2147483648 (2GB)
|
||||||
|
Stack Size: 5242880 (5MB)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Progressive Web App:**
|
||||||
|
```
|
||||||
|
PWA: ✓
|
||||||
|
Icon 144x144: res://icons/icon-144.png
|
||||||
|
Icon 180x180: res://icons/icon-180.png
|
||||||
|
Icon 512x512: res://icons/icon-512.png
|
||||||
|
Background Color: #000000
|
||||||
|
Orientation: landscape
|
||||||
|
```
|
||||||
|
|
||||||
|
**Compression:**
|
||||||
|
```
|
||||||
|
Export Type: Regular
|
||||||
|
Gzip Compression: ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Serve with Python
|
||||||
|
cd builds/web
|
||||||
|
python3 -m http.server 8000
|
||||||
|
|
||||||
|
# Or with Node
|
||||||
|
npx http-server builds/web -p 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:8000`
|
||||||
|
|
||||||
|
### Web Deployment
|
||||||
|
|
||||||
|
**Static Hosting (Recommended):**
|
||||||
|
```bash
|
||||||
|
# Netlify
|
||||||
|
netlify deploy --dir=builds/web --prod
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
vercel --prod builds/web
|
||||||
|
|
||||||
|
# GitHub Pages
|
||||||
|
git subtree push --prefix builds/web origin gh-pages
|
||||||
|
|
||||||
|
# Firebase Hosting
|
||||||
|
firebase deploy --only hosting
|
||||||
|
```
|
||||||
|
|
||||||
|
**itch.io:**
|
||||||
|
```
|
||||||
|
1. Zip the web folder
|
||||||
|
2. Upload to itch.io
|
||||||
|
3. Set "This file will be played in the browser"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Your Own Server:**
|
||||||
|
```nginx
|
||||||
|
# Nginx configuration
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourgame.com;
|
||||||
|
root /var/www/yourgame;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable CORS if needed
|
||||||
|
location ~* \.(wasm|pck)$ {
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types application/wasm application/octet-stream;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Best Practices
|
||||||
|
|
||||||
|
**Loading Screen:**
|
||||||
|
```html
|
||||||
|
<!-- builds/web/index.html -->
|
||||||
|
<style>
|
||||||
|
.loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="loading">Loading Your Game...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimize Size:**
|
||||||
|
```gdscript
|
||||||
|
# Disable unused features in export preset
|
||||||
|
# Compress textures
|
||||||
|
# Use streaming for audio
|
||||||
|
# Lazy load assets
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Android Export
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Android SDK:** Android 6.0+ (API 23+)
|
||||||
|
- **Android NDK:** r23b+
|
||||||
|
- **JDK:** OpenJDK 11+
|
||||||
|
- **Export Template:** Android template
|
||||||
|
- **Keystore:** For signing (release builds)
|
||||||
|
|
||||||
|
### Setup Android Development
|
||||||
|
|
||||||
|
**1. Install Android SDK:**
|
||||||
|
```bash
|
||||||
|
# Download Android Studio or command-line tools
|
||||||
|
# Set ANDROID_HOME environment variable
|
||||||
|
export ANDROID_HOME=$HOME/Android/Sdk
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Install Required Components:**
|
||||||
|
```bash
|
||||||
|
sdkmanager "platform-tools" "platforms;android-33" "build-tools;33.0.0" "ndk;23.2.8568313"
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Configure AeThex:**
|
||||||
|
```
|
||||||
|
Editor → Editor Settings → Export → Android:
|
||||||
|
Android SDK Path: /path/to/Android/Sdk
|
||||||
|
Debug Keystore: ~/.android/debug.keystore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Steps
|
||||||
|
|
||||||
|
**1. Add Export Preset:**
|
||||||
|
```
|
||||||
|
Project → Export
|
||||||
|
Click "Add..." → Android
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configure Options:**
|
||||||
|
```
|
||||||
|
Export Path: builds/android/YourGame.apk (or .aab)
|
||||||
|
|
||||||
|
Package:
|
||||||
|
Unique Name: com.yourcompany.yourgame
|
||||||
|
Name: Your Game
|
||||||
|
Signed: ✓ (for release)
|
||||||
|
|
||||||
|
Version:
|
||||||
|
Code: 1 (increment for each release)
|
||||||
|
Name: 1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Code Signing:**
|
||||||
|
```
|
||||||
|
Keystore:
|
||||||
|
Debug: ~/.android/debug.keystore
|
||||||
|
Release: /path/to/release.keystore
|
||||||
|
User: your_key_alias
|
||||||
|
Password: your_keystore_password
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Export:**
|
||||||
|
```
|
||||||
|
Click "Export Project"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android-Specific Options
|
||||||
|
|
||||||
|
**Graphics:**
|
||||||
|
```
|
||||||
|
OpenGL ES: 3.0
|
||||||
|
ASTC Compression: ✓ (for textures)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permissions:**
|
||||||
|
```
|
||||||
|
Internet: ✓ (for cloud features)
|
||||||
|
Access Network State: ✓
|
||||||
|
Access WiFi State: ✓
|
||||||
|
Vibrate: ✓ (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Screen:**
|
||||||
|
```
|
||||||
|
Orientation: landscape (or portrait)
|
||||||
|
Support Small Screen: ✗
|
||||||
|
Support Normal Screen: ✓
|
||||||
|
Support Large Screen: ✓
|
||||||
|
Support XLarge Screen: ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
```
|
||||||
|
Export ABIs:
|
||||||
|
✓ armeabi-v7a (32-bit ARM)
|
||||||
|
✓ arm64-v8a (64-bit ARM) - Required by Play Store
|
||||||
|
✗ x86 (optional for emulators)
|
||||||
|
✗ x86_64 (optional for emulators)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Debug Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export unsigned debug APK
|
||||||
|
# Install via ADB
|
||||||
|
adb install builds/android/YourGame-debug.apk
|
||||||
|
|
||||||
|
# Run and view logs
|
||||||
|
adb logcat | grep YourGame
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Release Build
|
||||||
|
|
||||||
|
**1. Create Release Keystore:**
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore release.keystore -alias my_game_key \
|
||||||
|
-keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
|
||||||
|
# Keep this keystore safe! You need it for all future updates
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Export Release Build:**
|
||||||
|
```
|
||||||
|
Select "Android" preset
|
||||||
|
Enable "Signed"
|
||||||
|
Provide keystore details
|
||||||
|
Click "Export Project"
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Generate AAB (for Play Store):**
|
||||||
|
```
|
||||||
|
Export Path: builds/android/YourGame.aab
|
||||||
|
Export Type: AAB (Android App Bundle)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing on Device
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable USB debugging on Android device
|
||||||
|
# Connect via USB
|
||||||
|
|
||||||
|
# List devices
|
||||||
|
adb devices
|
||||||
|
|
||||||
|
# Install
|
||||||
|
adb install YourGame.apk
|
||||||
|
|
||||||
|
# Uninstall
|
||||||
|
adb uninstall com.yourcompany.yourgame
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
adb logcat -c # Clear logs
|
||||||
|
adb logcat | grep YourGame
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android Distribution
|
||||||
|
|
||||||
|
**Google Play Store:**
|
||||||
|
```
|
||||||
|
1. Create Play Console account
|
||||||
|
2. Create app listing
|
||||||
|
3. Upload AAB (not APK)
|
||||||
|
4. Configure store presence
|
||||||
|
5. Set pricing
|
||||||
|
6. Submit for review
|
||||||
|
```
|
||||||
|
|
||||||
|
**Other Stores:**
|
||||||
|
- Amazon Appstore (APK)
|
||||||
|
- Samsung Galaxy Store (APK)
|
||||||
|
- itch.io (APK)
|
||||||
|
- Direct download (APK)
|
||||||
|
|
||||||
|
### Android Optimization
|
||||||
|
|
||||||
|
**Reduce APK Size:**
|
||||||
|
```
|
||||||
|
# In export preset:
|
||||||
|
- Enable compression
|
||||||
|
- Disable unused ABIs
|
||||||
|
- Compress textures (ASTC)
|
||||||
|
- Remove unused resources
|
||||||
|
- Use ProGuard/R8
|
||||||
|
```
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
```gdscript
|
||||||
|
# Target 60 FPS on mid-range devices
|
||||||
|
# Test on:
|
||||||
|
# - Low-end device (2GB RAM)
|
||||||
|
# - Mid-range device (4GB RAM)
|
||||||
|
# - High-end device (8GB+ RAM)
|
||||||
|
|
||||||
|
# Android-specific optimizations:
|
||||||
|
func _ready():
|
||||||
|
if OS.get_name() == "Android":
|
||||||
|
# Reduce particle count
|
||||||
|
# Lower shadow quality
|
||||||
|
# Disable post-processing effects
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Platform Tips
|
||||||
|
|
||||||
|
### Asset Optimization
|
||||||
|
|
||||||
|
**Textures:**
|
||||||
|
```
|
||||||
|
Desktop: PNG, JPEG, or uncompressed
|
||||||
|
Web: Compress, reduce resolution
|
||||||
|
Mobile: ASTC compression, aggressive optimization
|
||||||
|
```
|
||||||
|
|
||||||
|
**Audio:**
|
||||||
|
```
|
||||||
|
Music: OGG Vorbis, 128kbps
|
||||||
|
SFX: OGG Vorbis, 64kbps
|
||||||
|
Mobile: Lower bitrates
|
||||||
|
```
|
||||||
|
|
||||||
|
**3D Models:**
|
||||||
|
```
|
||||||
|
Desktop: Full detail
|
||||||
|
Web: Reduce poly count 30%
|
||||||
|
Mobile: Reduce poly count 50%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform Detection
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
match OS.get_name():
|
||||||
|
"Windows":
|
||||||
|
setup_windows()
|
||||||
|
"Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD":
|
||||||
|
setup_linux()
|
||||||
|
"macOS":
|
||||||
|
setup_macos()
|
||||||
|
"Web":
|
||||||
|
setup_web()
|
||||||
|
"Android":
|
||||||
|
setup_android()
|
||||||
|
"iOS":
|
||||||
|
setup_ios()
|
||||||
|
|
||||||
|
func setup_windows():
|
||||||
|
# Windows-specific setup
|
||||||
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
|
||||||
|
|
||||||
|
func setup_mobile():
|
||||||
|
# Touch controls, battery optimization
|
||||||
|
if OS.get_name() in ["Android", "iOS"]:
|
||||||
|
# Mobile optimizations
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feature Flags
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
const FEATURES = {
|
||||||
|
"cloud_saves": true,
|
||||||
|
"multiplayer": true,
|
||||||
|
"analytics": true,
|
||||||
|
"haptics": OS.get_name() in ["Android", "iOS"],
|
||||||
|
"keyboard": OS.get_name() not in ["Android", "iOS", "Web"],
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if FEATURES.cloud_saves:
|
||||||
|
await AeThexCloud.connect_to_cloud()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Export template missing:**
|
||||||
|
```
|
||||||
|
Solution: Download export templates from
|
||||||
|
Editor → Export Templates → Download
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code signing failed (macOS):**
|
||||||
|
```bash
|
||||||
|
# Verify certificate
|
||||||
|
security find-identity -v -p codesigning
|
||||||
|
|
||||||
|
# Check entitlements
|
||||||
|
codesign -d --entitlements :- YourGame.app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Web build doesn't load:**
|
||||||
|
```
|
||||||
|
Check console for errors
|
||||||
|
Must be served from HTTP server (not file://)
|
||||||
|
Check SharedArrayBuffer requirements
|
||||||
|
Enable CORS headers if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Android build fails:**
|
||||||
|
```bash
|
||||||
|
# Check SDK paths
|
||||||
|
echo $ANDROID_HOME
|
||||||
|
|
||||||
|
# Update build tools
|
||||||
|
sdkmanager --update
|
||||||
|
|
||||||
|
# Clean build
|
||||||
|
./gradlew clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Testing
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Add FPS counter
|
||||||
|
func _process(delta):
|
||||||
|
if OS.is_debug_build():
|
||||||
|
$FPSLabel.text = "FPS: " + str(Engine.get_frames_per_second())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Size Optimization
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Disable unused modules in export preset
|
||||||
|
2. Compress textures appropriately
|
||||||
|
3. Use streaming for large assets
|
||||||
|
4. Remove debug symbols (release mode)
|
||||||
|
5. Use platform-specific compression
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Before Every Release
|
||||||
|
|
||||||
|
✓ Test on all target platforms
|
||||||
|
✓ Profile performance
|
||||||
|
✓ Check memory usage
|
||||||
|
✓ Test on low-end hardware
|
||||||
|
✓ Verify all assets load correctly
|
||||||
|
✓ Test input methods (keyboard, gamepad, touch)
|
||||||
|
✓ Check for crash logs
|
||||||
|
|
||||||
|
### Versioning
|
||||||
|
|
||||||
|
```
|
||||||
|
Use semantic versioning: MAJOR.MINOR.PATCH
|
||||||
|
Example: 1.2.3
|
||||||
|
|
||||||
|
MAJOR: Breaking changes
|
||||||
|
MINOR: New features (backward compatible)
|
||||||
|
PATCH: Bug fixes
|
||||||
|
|
||||||
|
Android version code: Increment for each release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions example
|
||||||
|
name: Export Game
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
export:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Export for Windows
|
||||||
|
run: |
|
||||||
|
# Export commands here
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: game-builds
|
||||||
|
path: builds/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform Comparison
|
||||||
|
|
||||||
|
| Feature | Windows | Linux | macOS | Web | Android |
|
||||||
|
|---------|---------|-------|-------|-----|---------|
|
||||||
|
| **Ease of Export** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| **Distribution** | Easy | Easy | Medium | Easiest | Medium |
|
||||||
|
| **Performance** | Excellent | Excellent | Excellent | Good | Good |
|
||||||
|
| **File Size** | Medium | Medium | Large | Large | Medium |
|
||||||
|
| **Monetization** | Direct | Direct | App Store | Ads/IAP | Play Store |
|
||||||
|
| **Updates** | Manual | Manual | Manual | Instant | Store |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **Test Your Exports:** Always test on real hardware
|
||||||
|
- **Get Feedback:** Share with beta testers
|
||||||
|
- **Optimize:** Profile and improve performance
|
||||||
|
- **Distribute:** Choose your distribution method
|
||||||
|
- **Monitor:** Use AeThexAnalytics to track usage
|
||||||
|
|
||||||
|
For platform-specific questions, see:
|
||||||
|
- [API Reference](API_REFERENCE.md) - Cloud features
|
||||||
|
- [GAME_DEVELOPMENT.md](GAME_DEVELOPMENT.md) - Core engine features
|
||||||
|
- [ARCHITECTURE_OVERVIEW.md](ARCHITECTURE_OVERVIEW.md) - Technical details
|
||||||
|
|
||||||
|
Happy shipping! 🚀
|
||||||
724
docs/GAME_DEVELOPMENT.md
Normal file
724
docs/GAME_DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,724 @@
|
||||||
|
# Game Development in AeThex
|
||||||
|
|
||||||
|
This guide covers the core game development concepts and systems in AeThex Engine.
|
||||||
|
|
||||||
|
## Scene System
|
||||||
|
|
||||||
|
### What is a Scene?
|
||||||
|
|
||||||
|
A **scene** is a collection of nodes organized in a tree structure. Scenes are the building blocks of your game - they can represent:
|
||||||
|
- Game levels
|
||||||
|
- UI screens
|
||||||
|
- Characters
|
||||||
|
- Items
|
||||||
|
- Prefabs/templates
|
||||||
|
|
||||||
|
### Working with Scenes
|
||||||
|
|
||||||
|
**Creating Scenes:**
|
||||||
|
```gdscript
|
||||||
|
# Load a scene
|
||||||
|
var scene = load("res://levels/main_level.tscn")
|
||||||
|
|
||||||
|
# Instance a scene
|
||||||
|
var instance = scene.instantiate()
|
||||||
|
|
||||||
|
# Add to scene tree
|
||||||
|
add_child(instance)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Switching Scenes:**
|
||||||
|
```gdscript
|
||||||
|
# Change to a new scene
|
||||||
|
get_tree().change_scene_to_file("res://levels/next_level.tscn")
|
||||||
|
|
||||||
|
# Or using a packed scene
|
||||||
|
var packed_scene = load("res://levels/next_level.tscn")
|
||||||
|
get_tree().change_scene_to_packed(packed_scene)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scene Lifecycle:**
|
||||||
|
- `_enter_tree()` - Called when node enters the scene tree
|
||||||
|
- `_ready()` - Called when node and children are ready
|
||||||
|
- `_exit_tree()` - Called when node exits the scene tree
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node Hierarchy
|
||||||
|
|
||||||
|
### Understanding Nodes
|
||||||
|
|
||||||
|
**Nodes** are the fundamental building blocks in AeThex. Everything in your game is a node or inherits from the Node class.
|
||||||
|
|
||||||
|
### Node Types
|
||||||
|
|
||||||
|
**Common Node Classes:**
|
||||||
|
- `Node` - Base class for all scene objects
|
||||||
|
- `Node2D` - Base for all 2D game objects (has position, rotation, scale)
|
||||||
|
- `Node3D` - Base for all 3D game objects
|
||||||
|
- `Control` - Base for all UI elements
|
||||||
|
- `CanvasItem` - Base for anything that can be drawn
|
||||||
|
|
||||||
|
### Working with Node Hierarchy
|
||||||
|
|
||||||
|
**Adding/Removing Nodes:**
|
||||||
|
```gdscript
|
||||||
|
# Add a child node
|
||||||
|
var sprite = Sprite2D.new()
|
||||||
|
add_child(sprite)
|
||||||
|
|
||||||
|
# Remove a child
|
||||||
|
sprite.queue_free() # Deferred removal (safe)
|
||||||
|
sprite.free() # Immediate removal (use carefully)
|
||||||
|
|
||||||
|
# Get parent
|
||||||
|
var parent = get_parent()
|
||||||
|
|
||||||
|
# Reparent a node
|
||||||
|
sprite.reparent(new_parent)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Finding Nodes:**
|
||||||
|
```gdscript
|
||||||
|
# Get a child by name
|
||||||
|
var player = get_node("Player")
|
||||||
|
# Or using shorthand
|
||||||
|
var player = $Player
|
||||||
|
|
||||||
|
# Get a node by path
|
||||||
|
var weapon = $Player/Weapon
|
||||||
|
var health = get_node("Player/Health")
|
||||||
|
|
||||||
|
# Find nodes by group
|
||||||
|
var enemies = get_tree().get_nodes_in_group("enemies")
|
||||||
|
|
||||||
|
# Find parent by type
|
||||||
|
var level = find_parent("Level")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node Groups:**
|
||||||
|
```gdscript
|
||||||
|
# Add to a group
|
||||||
|
add_to_group("enemies")
|
||||||
|
add_to_group("damageable")
|
||||||
|
|
||||||
|
# Check if in group
|
||||||
|
if is_in_group("enemies"):
|
||||||
|
print("This is an enemy!")
|
||||||
|
|
||||||
|
# Remove from group
|
||||||
|
remove_from_group("enemies")
|
||||||
|
|
||||||
|
# Call function on all nodes in group
|
||||||
|
get_tree().call_group("enemies", "take_damage", 10)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signals and Callbacks
|
||||||
|
|
||||||
|
### What are Signals?
|
||||||
|
|
||||||
|
**Signals** are AeThex's implementation of the observer pattern. They allow nodes to communicate without tight coupling.
|
||||||
|
|
||||||
|
### Built-in Signals
|
||||||
|
|
||||||
|
**Common Node Signals:**
|
||||||
|
- `ready` - Emitted when node is ready
|
||||||
|
- `tree_entered` - Node entered the scene tree
|
||||||
|
- `tree_exited` - Node left the scene tree
|
||||||
|
|
||||||
|
**Input Signals:**
|
||||||
|
- `Area2D.body_entered(body)` - Another body entered the area
|
||||||
|
- `Area2D.body_exited(body)` - Another body left the area
|
||||||
|
- `Button.pressed()` - Button was clicked
|
||||||
|
|
||||||
|
### Creating Custom Signals
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Declare signals
|
||||||
|
signal health_changed(new_health)
|
||||||
|
signal player_died
|
||||||
|
signal item_collected(item_name, quantity)
|
||||||
|
|
||||||
|
var health = 100
|
||||||
|
|
||||||
|
func take_damage(amount):
|
||||||
|
health -= amount
|
||||||
|
emit_signal("health_changed", health)
|
||||||
|
|
||||||
|
if health <= 0:
|
||||||
|
emit_signal("player_died")
|
||||||
|
|
||||||
|
func collect_item(item, qty):
|
||||||
|
emit_signal("item_collected", item, qty)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connecting to Signals
|
||||||
|
|
||||||
|
**Code-based Connections:**
|
||||||
|
```gdscript
|
||||||
|
# Connect to a signal
|
||||||
|
player.health_changed.connect(_on_health_changed)
|
||||||
|
|
||||||
|
# With custom parameters
|
||||||
|
player.health_changed.connect(_on_health_changed.bind("extra_param"))
|
||||||
|
|
||||||
|
# One-shot connection (auto-disconnects after first emit)
|
||||||
|
player.health_changed.connect(_on_health_changed, CONNECT_ONE_SHOT)
|
||||||
|
|
||||||
|
# Disconnect from signal
|
||||||
|
player.health_changed.disconnect(_on_health_changed)
|
||||||
|
|
||||||
|
func _on_health_changed(new_health):
|
||||||
|
print("Health is now: ", new_health)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lambda Connections:**
|
||||||
|
```gdscript
|
||||||
|
button.pressed.connect(func(): print("Button clicked!"))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Physics
|
||||||
|
|
||||||
|
### 2D Physics
|
||||||
|
|
||||||
|
**RigidBody2D** - Dynamic physics body affected by forces:
|
||||||
|
```gdscript
|
||||||
|
extends RigidBody2D
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Set physics properties
|
||||||
|
mass = 10.0
|
||||||
|
gravity_scale = 1.0
|
||||||
|
linear_damp = 0.1
|
||||||
|
angular_damp = 0.1
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
# Apply force
|
||||||
|
apply_force(Vector2(100, 0))
|
||||||
|
|
||||||
|
# Apply impulse (instant)
|
||||||
|
apply_impulse(Vector2(0, -500))
|
||||||
|
|
||||||
|
# Apply torque (rotation)
|
||||||
|
apply_torque(100)
|
||||||
|
```
|
||||||
|
|
||||||
|
**StaticBody2D** - Immovable physics body (walls, floors):
|
||||||
|
```gdscript
|
||||||
|
extends StaticBody2D
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Static bodies don't move
|
||||||
|
# Used for terrain, walls, platforms
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**CharacterBody2D** - For player-controlled movement:
|
||||||
|
```gdscript
|
||||||
|
extends CharacterBody2D
|
||||||
|
|
||||||
|
var speed = 300.0
|
||||||
|
var jump_velocity = -400.0
|
||||||
|
var gravity = 980.0
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
# Apply gravity
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y += gravity * delta
|
||||||
|
|
||||||
|
# Handle jump
|
||||||
|
if Input.is_action_just_pressed("jump") and is_on_floor():
|
||||||
|
velocity.y = jump_velocity
|
||||||
|
|
||||||
|
# Get input direction
|
||||||
|
var direction = Input.get_axis("move_left", "move_right")
|
||||||
|
velocity.x = direction * speed
|
||||||
|
|
||||||
|
# Move with collision detection
|
||||||
|
move_and_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3D Physics
|
||||||
|
|
||||||
|
**RigidBody3D:**
|
||||||
|
```gdscript
|
||||||
|
extends RigidBody3D
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
mass = 10.0
|
||||||
|
gravity_scale = 1.0
|
||||||
|
|
||||||
|
func apply_jump():
|
||||||
|
apply_central_impulse(Vector3.UP * 500)
|
||||||
|
```
|
||||||
|
|
||||||
|
**CharacterBody3D:**
|
||||||
|
```gdscript
|
||||||
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
var speed = 5.0
|
||||||
|
var jump_strength = 10.0
|
||||||
|
var gravity = 20.0
|
||||||
|
|
||||||
|
func _physics_process(delta):
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y -= gravity * delta
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("jump") and is_on_floor():
|
||||||
|
velocity.y = jump_strength
|
||||||
|
|
||||||
|
var input_dir = Input.get_vector("left", "right", "forward", "back")
|
||||||
|
var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
||||||
|
|
||||||
|
velocity.x = direction.x * speed
|
||||||
|
velocity.z = direction.z * speed
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collision Shapes
|
||||||
|
|
||||||
|
**Adding Collision Shapes:**
|
||||||
|
```gdscript
|
||||||
|
# Collision shapes must be children of physics bodies
|
||||||
|
# Common shapes:
|
||||||
|
# - CollisionShape2D / CollisionShape3D
|
||||||
|
# - RectangleShape2D, CircleShape2D, CapsuleShape2D
|
||||||
|
# - BoxShape3D, SphereShape3D, CapsuleShape3D
|
||||||
|
|
||||||
|
# In code:
|
||||||
|
var collision = CollisionShape2D.new()
|
||||||
|
var shape = CircleShape2D.new()
|
||||||
|
shape.radius = 32
|
||||||
|
collision.shape = shape
|
||||||
|
add_child(collision)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Physics Layers
|
||||||
|
|
||||||
|
**Collision Layers & Masks:**
|
||||||
|
```gdscript
|
||||||
|
# Layer: What layers this body is on (up to 32)
|
||||||
|
collision_layer = 0b0001 # Layer 1
|
||||||
|
|
||||||
|
# Mask: What layers this body can collide with
|
||||||
|
collision_mask = 0b0010 # Can collide with layer 2
|
||||||
|
|
||||||
|
# Common setup:
|
||||||
|
# Layer 1: Player
|
||||||
|
# Layer 2: Enemies
|
||||||
|
# Layer 3: Environment
|
||||||
|
# Layer 4: Collectibles
|
||||||
|
|
||||||
|
# Player collides with enemies and environment:
|
||||||
|
collision_layer = 1 # Binary: 0001
|
||||||
|
collision_mask = 6 # Binary: 0110 (layers 2 and 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Checking Collisions:**
|
||||||
|
```gdscript
|
||||||
|
# CharacterBody2D/3D
|
||||||
|
func _physics_process(delta):
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
for i in get_slide_collision_count():
|
||||||
|
var collision = get_slide_collision(i)
|
||||||
|
print("Collided with: ", collision.get_collider().name)
|
||||||
|
|
||||||
|
# Area2D/3D signals
|
||||||
|
func _ready():
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
|
func _on_body_entered(body):
|
||||||
|
if body.is_in_group("player"):
|
||||||
|
print("Player entered area!")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI System
|
||||||
|
|
||||||
|
### Control Nodes
|
||||||
|
|
||||||
|
**Control** is the base class for all UI elements. Common UI nodes:
|
||||||
|
|
||||||
|
**Buttons:**
|
||||||
|
- `Button` - Standard push button
|
||||||
|
- `CheckButton` - Toggle button
|
||||||
|
- `OptionButton` - Dropdown menu
|
||||||
|
|
||||||
|
**Text:**
|
||||||
|
- `Label` - Display text
|
||||||
|
- `RichTextLabel` - Text with formatting (BBCode)
|
||||||
|
- `LineEdit` - Single-line text input
|
||||||
|
- `TextEdit` - Multi-line text input
|
||||||
|
|
||||||
|
**Containers:**
|
||||||
|
- `VBoxContainer` - Vertical layout
|
||||||
|
- `HBoxContainer` - Horizontal layout
|
||||||
|
- `GridContainer` - Grid layout
|
||||||
|
- `MarginContainer` - Adds margins
|
||||||
|
- `PanelContainer` - Background panel
|
||||||
|
|
||||||
|
### Basic UI Example
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Create a button
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = "Click Me"
|
||||||
|
button.pressed.connect(_on_button_pressed)
|
||||||
|
add_child(button)
|
||||||
|
|
||||||
|
# Create a label
|
||||||
|
var label = Label.new()
|
||||||
|
label.text = "Score: 0"
|
||||||
|
add_child(label)
|
||||||
|
|
||||||
|
func _on_button_pressed():
|
||||||
|
print("Button clicked!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layouts and Containers
|
||||||
|
|
||||||
|
**Automatic Layouts:**
|
||||||
|
```gdscript
|
||||||
|
# VBoxContainer - stacks children vertically
|
||||||
|
var vbox = VBoxContainer.new()
|
||||||
|
vbox.add_child(Button.new())
|
||||||
|
vbox.add_child(Button.new())
|
||||||
|
vbox.add_child(Button.new())
|
||||||
|
|
||||||
|
# HBoxContainer - arranges children horizontally
|
||||||
|
var hbox = HBoxContainer.new()
|
||||||
|
hbox.add_theme_constant_override("separation", 10) # 10px spacing
|
||||||
|
|
||||||
|
# GridContainer - grid layout
|
||||||
|
var grid = GridContainer.new()
|
||||||
|
grid.columns = 3
|
||||||
|
for i in 9:
|
||||||
|
grid.add_child(Button.new())
|
||||||
|
```
|
||||||
|
|
||||||
|
**Anchors and Margins:**
|
||||||
|
```gdscript
|
||||||
|
# Anchors determine where control is positioned (0.0 to 1.0)
|
||||||
|
var panel = Panel.new()
|
||||||
|
|
||||||
|
# Center the panel
|
||||||
|
panel.anchor_left = 0.5
|
||||||
|
panel.anchor_top = 0.5
|
||||||
|
panel.anchor_right = 0.5
|
||||||
|
panel.anchor_bottom = 0.5
|
||||||
|
|
||||||
|
# Offset from anchor point
|
||||||
|
panel.offset_left = -100
|
||||||
|
panel.offset_top = -50
|
||||||
|
panel.offset_right = 100
|
||||||
|
panel.offset_bottom = 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Themes
|
||||||
|
|
||||||
|
**Applying Themes:**
|
||||||
|
```gdscript
|
||||||
|
# Load a theme
|
||||||
|
var theme = load("res://themes/main_theme.tres")
|
||||||
|
theme = theme # Apply to root Control
|
||||||
|
|
||||||
|
# Or set individual theme overrides
|
||||||
|
var button = Button.new()
|
||||||
|
button.add_theme_color_override("font_color", Color.CYAN)
|
||||||
|
button.add_theme_font_size_override("font_size", 24)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsive Design
|
||||||
|
|
||||||
|
**Size Flags:**
|
||||||
|
```gdscript
|
||||||
|
var label = Label.new()
|
||||||
|
|
||||||
|
# Expand horizontally
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
# Shrink to content
|
||||||
|
label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
```
|
||||||
|
|
||||||
|
**Handling Resize:**
|
||||||
|
```gdscript
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
get_viewport().size_changed.connect(_on_viewport_resized)
|
||||||
|
|
||||||
|
func _on_viewport_resized():
|
||||||
|
var viewport_size = get_viewport_rect().size
|
||||||
|
print("New size: ", viewport_size)
|
||||||
|
# Adjust UI layout
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audio System
|
||||||
|
|
||||||
|
### AudioStreamPlayer
|
||||||
|
|
||||||
|
Three types of audio players:
|
||||||
|
- `AudioStreamPlayer` - 2D positional audio
|
||||||
|
- `AudioStreamPlayer2D` - 2D positional audio
|
||||||
|
- `AudioStreamPlayer3D` - 3D spatial audio
|
||||||
|
|
||||||
|
### Playing Sounds
|
||||||
|
|
||||||
|
**Basic Audio:**
|
||||||
|
```gdscript
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
@onready var sfx_player = $AudioStreamPlayer
|
||||||
|
@onready var music_player = $MusicPlayer
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Load and play sound
|
||||||
|
var sound = load("res://sounds/jump.ogg")
|
||||||
|
sfx_player.stream = sound
|
||||||
|
sfx_player.play()
|
||||||
|
|
||||||
|
func play_sound(sound_path: String):
|
||||||
|
sfx_player.stream = load(sound_path)
|
||||||
|
sfx_player.play()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Music Management
|
||||||
|
|
||||||
|
**Background Music:**
|
||||||
|
```gdscript
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var current_music = null
|
||||||
|
var music_player = AudioStreamPlayer.new()
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
add_child(music_player)
|
||||||
|
music_player.finished.connect(_on_music_finished)
|
||||||
|
|
||||||
|
func play_music(music_path: String, loop: bool = true):
|
||||||
|
var music = load(music_path)
|
||||||
|
music_player.stream = music
|
||||||
|
music_player.play()
|
||||||
|
|
||||||
|
if loop:
|
||||||
|
music_player.finished.connect(func(): music_player.play())
|
||||||
|
|
||||||
|
current_music = music_path
|
||||||
|
|
||||||
|
func stop_music():
|
||||||
|
music_player.stop()
|
||||||
|
|
||||||
|
func fade_out_music(duration: float = 1.0):
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(music_player, "volume_db", -80, duration)
|
||||||
|
tween.tween_callback(stop_music)
|
||||||
|
|
||||||
|
func fade_in_music(duration: float = 1.0):
|
||||||
|
music_player.volume_db = -80
|
||||||
|
music_player.play()
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(music_player, "volume_db", 0, duration)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sound Effects
|
||||||
|
|
||||||
|
**Sound Effect Pool:**
|
||||||
|
```gdscript
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var sfx_players = []
|
||||||
|
var pool_size = 8
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Create a pool of audio players
|
||||||
|
for i in pool_size:
|
||||||
|
var player = AudioStreamPlayer.new()
|
||||||
|
add_child(player)
|
||||||
|
sfx_players.append(player)
|
||||||
|
|
||||||
|
func play_sfx(sound_path: String, volume_db: float = 0.0):
|
||||||
|
# Find available player
|
||||||
|
for player in sfx_players:
|
||||||
|
if not player.playing:
|
||||||
|
player.stream = load(sound_path)
|
||||||
|
player.volume_db = volume_db
|
||||||
|
player.play()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If all busy, use first one
|
||||||
|
sfx_players[0].stream = load(sound_path)
|
||||||
|
sfx_players[0].volume_db = volume_db
|
||||||
|
sfx_players[0].play()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3D Audio
|
||||||
|
|
||||||
|
**Spatial Audio:**
|
||||||
|
```gdscript
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@onready var audio_3d = $AudioStreamPlayer3D
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Configure 3D audio
|
||||||
|
audio_3d.max_distance = 50.0 # Max hearing distance
|
||||||
|
audio_3d.attenuation_model = AudioStreamPlayer3D.ATTENUATION_INVERSE_DISTANCE
|
||||||
|
audio_3d.unit_size = 1.0
|
||||||
|
|
||||||
|
# Play sound at this position
|
||||||
|
audio_3d.stream = load("res://sounds/explosion.ogg")
|
||||||
|
audio_3d.play()
|
||||||
|
|
||||||
|
func play_3d_sound_at(sound_path: String, position: Vector3):
|
||||||
|
var player = AudioStreamPlayer3D.new()
|
||||||
|
add_child(player)
|
||||||
|
player.global_position = position
|
||||||
|
player.stream = load(sound_path)
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
# Remove when finished
|
||||||
|
await player.finished
|
||||||
|
player.queue_free()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow & Tools
|
||||||
|
|
||||||
|
### Studio IDE Integration
|
||||||
|
|
||||||
|
AeThex Studio provides a full IDE experience with:
|
||||||
|
|
||||||
|
**Code Editor:**
|
||||||
|
- GDScript syntax highlighting
|
||||||
|
- Autocomplete and intellisense
|
||||||
|
- Inline documentation
|
||||||
|
- Error checking and linting
|
||||||
|
- Code folding and navigation
|
||||||
|
|
||||||
|
**Scene Tree:**
|
||||||
|
- Visual node hierarchy
|
||||||
|
- Drag-and-drop node creation
|
||||||
|
- Inspector panel for properties
|
||||||
|
- Live scene editing
|
||||||
|
|
||||||
|
**Asset Browser:**
|
||||||
|
- File system navigation
|
||||||
|
- Asset preview (images, 3D models)
|
||||||
|
- Drag-and-drop import
|
||||||
|
- Asset metadata
|
||||||
|
|
||||||
|
**Live Reload:**
|
||||||
|
```gdscript
|
||||||
|
# Changes are hot-reloaded automatically
|
||||||
|
# No need to restart the game
|
||||||
|
# Scripts reload on save
|
||||||
|
```
|
||||||
|
|
||||||
|
**Studio Bridge:**
|
||||||
|
```gdscript
|
||||||
|
# Check if running in Studio
|
||||||
|
if AeThexStudio.is_available():
|
||||||
|
print("Running in AeThex Studio")
|
||||||
|
|
||||||
|
# Send messages to Studio
|
||||||
|
AeThexStudio.log_message("Custom debug info")
|
||||||
|
|
||||||
|
# Open file in Studio
|
||||||
|
AeThexStudio.open_file("res://scripts/player.gd")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Control
|
||||||
|
|
||||||
|
**Git Integration:**
|
||||||
|
```bash
|
||||||
|
# Initialize repository
|
||||||
|
cd your_project
|
||||||
|
git init
|
||||||
|
|
||||||
|
# AeThex-specific .gitignore
|
||||||
|
echo ".import/" >> .gitignore
|
||||||
|
echo "*.import" >> .gitignore
|
||||||
|
echo ".godot/" >> .gitignore
|
||||||
|
echo "export_presets.cfg" >> .gitignore
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Collaboration Workflow:**
|
||||||
|
```bash
|
||||||
|
# Create feature branch
|
||||||
|
git checkout -b feature/player-movement
|
||||||
|
|
||||||
|
# Make changes and commit
|
||||||
|
git add scripts/player.gd
|
||||||
|
git commit -m "Implement player movement"
|
||||||
|
|
||||||
|
# Push branch
|
||||||
|
git push origin feature/player-movement
|
||||||
|
|
||||||
|
# Create pull request on GitHub
|
||||||
|
# Review and merge
|
||||||
|
```
|
||||||
|
|
||||||
|
**Managing Merge Conflicts:**
|
||||||
|
```bash
|
||||||
|
# Update from main
|
||||||
|
git checkout main
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Merge into feature branch
|
||||||
|
git checkout feature/player-movement
|
||||||
|
git merge main
|
||||||
|
|
||||||
|
# If conflicts occur, resolve them in editor
|
||||||
|
# Then:
|
||||||
|
git add .
|
||||||
|
git commit -m "Resolve merge conflicts"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Branching Strategy:**
|
||||||
|
- `main` - Stable, production-ready code
|
||||||
|
- `develop` - Integration branch for features
|
||||||
|
- `feature/*` - Individual features
|
||||||
|
- `hotfix/*` - Emergency fixes
|
||||||
|
- `release/*` - Release preparation
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
1. Commit often with clear messages
|
||||||
|
2. Use branches for new features
|
||||||
|
3. Keep commits atomic (one logical change)
|
||||||
|
4. Pull before pushing
|
||||||
|
5. Review code before merging
|
||||||
|
6. Use `.gitattributes` for binary files:
|
||||||
|
```
|
||||||
|
*.tscn merge=binary
|
||||||
|
*.tres merge=binary
|
||||||
|
*.asset merge=binary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **API Reference:** See [API_REFERENCE.md](API_REFERENCE.md) for AeThex cloud services
|
||||||
|
- **First Game Tutorial:** Build a complete game in [FIRST_GAME_TUTORIAL.md](tutorials/FIRST_GAME_TUTORIAL.md)
|
||||||
|
- **Studio Integration:** Learn more at [STUDIO_INTEGRATION.md](STUDIO_INTEGRATION.md)
|
||||||
|
- **Architecture:** Understand the engine at [ARCHITECTURE_OVERVIEW.md](ARCHITECTURE_OVERVIEW.md)
|
||||||
806
docs/PUBLISHING_GUIDE.md
Normal file
806
docs/PUBLISHING_GUIDE.md
Normal file
|
|
@ -0,0 +1,806 @@
|
||||||
|
# Publishing Your Game
|
||||||
|
|
||||||
|
A comprehensive guide to publishing and distributing your AeThex game to players worldwide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide covers the complete publishing workflow:
|
||||||
|
- Pre-launch checklist
|
||||||
|
- Platform-specific setup
|
||||||
|
- Store submissions
|
||||||
|
- Marketing materials
|
||||||
|
- Post-launch monitoring
|
||||||
|
- Updates and maintenance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Pre-Launch Checklist](#pre-launch-checklist)
|
||||||
|
2. [Export Your Game](#export-your-game)
|
||||||
|
3. [Platform-Specific Publishing](#platform-specific-publishing)
|
||||||
|
4. [Marketing Materials](#marketing-materials)
|
||||||
|
5. [Launch Strategy](#launch-strategy)
|
||||||
|
6. [Post-Launch](#post-launch)
|
||||||
|
7. [Updates and Patches](#updates-and-patches)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-Launch Checklist
|
||||||
|
|
||||||
|
### ✅ Game Completion
|
||||||
|
|
||||||
|
- [ ] All levels/content implemented
|
||||||
|
- [ ] Tutorial completed and tested
|
||||||
|
- [ ] All major bugs fixed
|
||||||
|
- [ ] Performance optimized for target platforms
|
||||||
|
- [ ] Tested on minimum spec hardware
|
||||||
|
- [ ] Audio balanced and finalized
|
||||||
|
- [ ] UI polished and consistent
|
||||||
|
- [ ] Accessibility options implemented
|
||||||
|
|
||||||
|
### ✅ Technical Requirements
|
||||||
|
|
||||||
|
- [ ] Cloud services configured (if using)
|
||||||
|
- [ ] Analytics integrated
|
||||||
|
- [ ] Crash reporting set up
|
||||||
|
- [ ] Save system tested thoroughly
|
||||||
|
- [ ] Multiplayer stress-tested (if applicable)
|
||||||
|
- [ ] All external APIs work in production
|
||||||
|
- [ ] Privacy policy created
|
||||||
|
- [ ] Terms of service written
|
||||||
|
|
||||||
|
### ✅ Legal & Business
|
||||||
|
|
||||||
|
- [ ] Company/entity registered (if required)
|
||||||
|
- [ ] Tax information prepared
|
||||||
|
- [ ] Age rating obtained (ESRB, PEGI, etc.)
|
||||||
|
- [ ] Trademark search completed
|
||||||
|
- [ ] Copyright notices added
|
||||||
|
- [ ] License agreements in place
|
||||||
|
- [ ] Insurance considered (if applicable)
|
||||||
|
|
||||||
|
### ✅ Store Assets
|
||||||
|
|
||||||
|
- [ ] Game title finalized
|
||||||
|
- [ ] Description written (all required languages)
|
||||||
|
- [ ] Screenshots captured (all required platforms)
|
||||||
|
- [ ] Trailer created and uploaded
|
||||||
|
- [ ] Icon/logo designed in all required sizes
|
||||||
|
- [ ] Banner images created
|
||||||
|
- [ ] Keywords/tags researched
|
||||||
|
- [ ] Store page previewed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export Your Game
|
||||||
|
|
||||||
|
See [EXPORTING_GAMES.md](EXPORTING_GAMES.md) for detailed export instructions for:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Web (HTML5)
|
||||||
|
- Android
|
||||||
|
- iOS (coming soon)
|
||||||
|
|
||||||
|
**Quick Export:**
|
||||||
|
```gdscript
|
||||||
|
# Via editor:
|
||||||
|
Project → Export → Select Platform → Export Project
|
||||||
|
|
||||||
|
# Via command line:
|
||||||
|
aethex --export-release "Windows Desktop" builds/windows/game.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Platform-Specific Publishing
|
||||||
|
|
||||||
|
### Steam
|
||||||
|
|
||||||
|
**1. Create Steamworks Account:**
|
||||||
|
- Go to [partner.steamgames.com](https://partner.steamgames.com)
|
||||||
|
- Pay app deposit ($100 per game, recoupable)
|
||||||
|
- Complete company verification
|
||||||
|
|
||||||
|
**2. Set Up Your Game:**
|
||||||
|
```
|
||||||
|
Steamworks Admin Panel:
|
||||||
|
├── App Admin → Basic Info
|
||||||
|
│ ├── Game name
|
||||||
|
│ ├── Description
|
||||||
|
│ └── Release date
|
||||||
|
├── Store Presence → Graphics
|
||||||
|
│ ├── Header capsule (460x215)
|
||||||
|
│ ├── Small capsule (231x87)
|
||||||
|
│ ├── Screenshots (1920x1080)
|
||||||
|
│ └── Trailer
|
||||||
|
└── Technical Requirements
|
||||||
|
├── Supported OS
|
||||||
|
└── Minimum specs
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Upload Build:**
|
||||||
|
```bash
|
||||||
|
# Install Steamworks SDK
|
||||||
|
# Use SteamPipe to upload
|
||||||
|
|
||||||
|
# steamcmd.exe
|
||||||
|
login your_username
|
||||||
|
set_product your_app_id
|
||||||
|
upload_depot depot_id depot_manifest.vdf
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Configure Store Page:**
|
||||||
|
- Description (short & long)
|
||||||
|
- Tags/categories
|
||||||
|
- Supported languages
|
||||||
|
- System requirements
|
||||||
|
- Pricing
|
||||||
|
|
||||||
|
**5. Submit for Review:**
|
||||||
|
- Complete all required fields
|
||||||
|
- Submit for Steam review (1-5 days)
|
||||||
|
- Address any feedback
|
||||||
|
- Set release date
|
||||||
|
|
||||||
|
**6. Launch:**
|
||||||
|
- Release when ready
|
||||||
|
- Consider Early Access for ongoing development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### itch.io
|
||||||
|
|
||||||
|
**1. Create Account:**
|
||||||
|
- Go to [itch.io](https://itch.io)
|
||||||
|
- Sign up for creator account (free)
|
||||||
|
|
||||||
|
**2. Create New Project:**
|
||||||
|
```
|
||||||
|
Dashboard → Create new project:
|
||||||
|
├── Title & URL
|
||||||
|
├── Classification (Game)
|
||||||
|
├── Kind (Downloadable/HTML5)
|
||||||
|
└── Release status
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Upload Files:**
|
||||||
|
```
|
||||||
|
Upload → Add file:
|
||||||
|
├── Windows ZIP
|
||||||
|
├── Linux TAR.GZ
|
||||||
|
├── macOS ZIP
|
||||||
|
└── Web folder (if HTML5)
|
||||||
|
|
||||||
|
Set file types:
|
||||||
|
- Windows: Executable
|
||||||
|
- Linux: Executable
|
||||||
|
- Web: This file will be played in the browser
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Configure Page:**
|
||||||
|
- Cover image (630x500)
|
||||||
|
- Screenshots
|
||||||
|
- Description
|
||||||
|
- Trailer embed
|
||||||
|
- Tags
|
||||||
|
- Pricing (free or paid)
|
||||||
|
- Donation options
|
||||||
|
|
||||||
|
**5. Publish:**
|
||||||
|
- Preview page
|
||||||
|
- Click "Publish"
|
||||||
|
- Share link immediately
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Free to publish
|
||||||
|
- Indie-friendly community
|
||||||
|
- Instant publishing
|
||||||
|
- Flexible pricing (pay what you want)
|
||||||
|
- Good for prototypes/demos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GOG
|
||||||
|
|
||||||
|
**1. Apply for Publishing:**
|
||||||
|
- Email [games@gog.com](mailto:games@gog.com)
|
||||||
|
- Provide game description and trailer
|
||||||
|
- Wait for approval (selective)
|
||||||
|
|
||||||
|
**2. Submission Process:**
|
||||||
|
- If approved, work with GOG partner manager
|
||||||
|
- Build must be DRM-free
|
||||||
|
- GOG handles QA testing
|
||||||
|
|
||||||
|
**3. Release:**
|
||||||
|
- GOG manages store page
|
||||||
|
- Revenue split: 70/30
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Epic Games Store
|
||||||
|
|
||||||
|
**1. Apply:**
|
||||||
|
- Go to [Epic Games Publishing](https://www.epicgames.com/unrealengine/en-US/publish)
|
||||||
|
- Submit application
|
||||||
|
- Wait for review
|
||||||
|
|
||||||
|
**2. Onboarding:**
|
||||||
|
- Complete developer agreement
|
||||||
|
- Set up payments
|
||||||
|
- Work with Epic partner manager
|
||||||
|
|
||||||
|
**3. Requirements:**
|
||||||
|
- High-quality polish expected
|
||||||
|
- Epic Games Account integration
|
||||||
|
- Achievement support recommended
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Google Play Store
|
||||||
|
|
||||||
|
**1. Create Developer Account:**
|
||||||
|
- Go to [Google Play Console](https://play.google.com/console)
|
||||||
|
- Pay one-time fee ($25)
|
||||||
|
- Complete account verification
|
||||||
|
|
||||||
|
**2. Create App:**
|
||||||
|
```
|
||||||
|
Play Console → Create app:
|
||||||
|
├── App details
|
||||||
|
├── Store listing
|
||||||
|
│ ├── Title
|
||||||
|
│ ├── Description (short & full)
|
||||||
|
│ ├── Screenshots (phone & tablet)
|
||||||
|
│ ├── Feature graphic (1024x500)
|
||||||
|
│ └── Icon (512x512)
|
||||||
|
├── Content rating (ESRB, PEGI)
|
||||||
|
└── Pricing & distribution
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Upload APK/AAB:**
|
||||||
|
```
|
||||||
|
Release Management → App releases:
|
||||||
|
├── Production → Create release
|
||||||
|
├── Upload AAB (recommended) or APK
|
||||||
|
├── Set version code/name
|
||||||
|
└── Add release notes
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Fill Requirements:**
|
||||||
|
- Privacy policy URL
|
||||||
|
- Content rating questionnaire
|
||||||
|
- Target audience
|
||||||
|
- App content
|
||||||
|
- Data safety section
|
||||||
|
|
||||||
|
**5. Submit for Review:**
|
||||||
|
- Review takes 1-7 days
|
||||||
|
- Address any policy violations
|
||||||
|
- Publish when approved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Apple App Store (iOS)
|
||||||
|
|
||||||
|
**1. Apple Developer Account:**
|
||||||
|
- Join [Apple Developer Program](https://developer.apple.com/programs/) ($99/year)
|
||||||
|
- Complete agreements
|
||||||
|
|
||||||
|
**2. App Store Connect:**
|
||||||
|
```
|
||||||
|
Create New App:
|
||||||
|
├── Platform (iOS)
|
||||||
|
├── Name
|
||||||
|
├── Primary language
|
||||||
|
├── Bundle ID
|
||||||
|
└── SKU
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Prepare App:**
|
||||||
|
```
|
||||||
|
App Information:
|
||||||
|
├── Name & subtitle
|
||||||
|
├── Privacy policy URL
|
||||||
|
├── Category
|
||||||
|
├── Screenshots (all device sizes)
|
||||||
|
├── Description
|
||||||
|
└── Keywords
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Build Upload:**
|
||||||
|
```bash
|
||||||
|
# Archive in Xcode
|
||||||
|
# Upload via Xcode or Transporter app
|
||||||
|
# Wait for processing (15-60 minutes)
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Submit for Review:**
|
||||||
|
- Select build
|
||||||
|
- Set release method (manual/automatic)
|
||||||
|
- Add version information
|
||||||
|
- Submit (review takes 1-2 days typically)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Web Publishing
|
||||||
|
|
||||||
|
**GitHub Pages:**
|
||||||
|
```bash
|
||||||
|
# Build for web
|
||||||
|
aethex --export "Web" builds/web/index.html
|
||||||
|
|
||||||
|
# Create gh-pages branch
|
||||||
|
git checkout -b gh-pages
|
||||||
|
cp -r builds/web/* .
|
||||||
|
git add .
|
||||||
|
git commit -m "Deploy game"
|
||||||
|
git push origin gh-pages
|
||||||
|
|
||||||
|
# Enable in repository settings
|
||||||
|
# Access at: https://username.github.io/repository
|
||||||
|
```
|
||||||
|
|
||||||
|
**Netlify:**
|
||||||
|
```bash
|
||||||
|
# Install Netlify CLI
|
||||||
|
npm install -g netlify-cli
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
netlify deploy --dir=builds/web --prod
|
||||||
|
|
||||||
|
# Or drag-and-drop in Netlify dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vercel:**
|
||||||
|
```bash
|
||||||
|
# Install Vercel CLI
|
||||||
|
npm install -g vercel
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
vercel builds/web --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
**Self-Hosting:**
|
||||||
|
```nginx
|
||||||
|
# nginx configuration
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourgame.com;
|
||||||
|
root /var/www/yourgame;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable CORS for WebAssembly
|
||||||
|
location ~* \.(wasm|pck)$ {
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|wasm|pck)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Marketing Materials
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
- Capture at 1920x1080 or higher
|
||||||
|
- Show gameplay, not menus
|
||||||
|
- Highlight key features
|
||||||
|
- Include UI elements
|
||||||
|
- Use variety (action, exploration, etc.)
|
||||||
|
- Add subtle branding watermark
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
```gdscript
|
||||||
|
# In-game screenshot system
|
||||||
|
func take_screenshot():
|
||||||
|
var img = get_viewport().get_texture().get_image()
|
||||||
|
img.save_png("user://screenshot_%s.png" % Time.get_unix_time_from_system())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trailer
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
- 0-5s: Hook (best gameplay moment)
|
||||||
|
- 5-15s: Core gameplay footage
|
||||||
|
- 15-30s: Features and variety
|
||||||
|
- 30-45s: Unique selling points
|
||||||
|
- 45-60s: Call to action + release date
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- Video editing: DaVinci Resolve (free)
|
||||||
|
- Screen recording: OBS Studio (free)
|
||||||
|
- Music: [Epidemic Sound](https://epidemicsound.com), [Artlist](https://artlist.io)
|
||||||
|
|
||||||
|
### Store Description
|
||||||
|
|
||||||
|
**Template:**
|
||||||
|
```
|
||||||
|
[One-sentence hook]
|
||||||
|
|
||||||
|
[2-3 sentence description of gameplay]
|
||||||
|
|
||||||
|
KEY FEATURES:
|
||||||
|
• Feature 1 (brief description)
|
||||||
|
• Feature 2 (brief description)
|
||||||
|
• Feature 3 (brief description)
|
||||||
|
• Feature 4 (brief description)
|
||||||
|
|
||||||
|
[Optional: Story/setting paragraph]
|
||||||
|
|
||||||
|
[System requirements or platform info]
|
||||||
|
|
||||||
|
[Social media links]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Launch Strategy
|
||||||
|
|
||||||
|
### Timing
|
||||||
|
|
||||||
|
**When to Launch:**
|
||||||
|
- Avoid major game releases
|
||||||
|
- Consider seasonal factors (summer slow, Q4 busy)
|
||||||
|
- Tuesday/Thursday often best for visibility
|
||||||
|
- Allow time for reviews/coverage
|
||||||
|
|
||||||
|
**Soft Launch:**
|
||||||
|
- Release to smaller region first
|
||||||
|
- Gather feedback
|
||||||
|
- Fix critical issues
|
||||||
|
- Full launch 1-2 weeks later
|
||||||
|
|
||||||
|
### Press Kit
|
||||||
|
|
||||||
|
Create a press kit at `yourgame.com/press`:
|
||||||
|
- Fact sheet (release date, platforms, price)
|
||||||
|
- Description
|
||||||
|
- Features
|
||||||
|
- Trailer embed
|
||||||
|
- Screenshots (zip download)
|
||||||
|
- Logo (multiple formats)
|
||||||
|
- Developer info
|
||||||
|
- Contact email
|
||||||
|
|
||||||
|
### Reaching Out
|
||||||
|
|
||||||
|
**Media Contacts:**
|
||||||
|
- Research relevant gaming sites/YouTubers
|
||||||
|
- Send personalized emails (not mass blasts)
|
||||||
|
- Provide steam keys/review copies
|
||||||
|
- Follow up once if no response
|
||||||
|
|
||||||
|
**Email Template:**
|
||||||
|
```
|
||||||
|
Subject: [Your Game Name] - [Genre] launching [Date]
|
||||||
|
|
||||||
|
Hi [Name],
|
||||||
|
|
||||||
|
I'm [your name], developer of [game name], a [genre] game
|
||||||
|
launching on [platform] on [date].
|
||||||
|
|
||||||
|
[One paragraph about what makes your game special]
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
• Feature 1
|
||||||
|
• Feature 2
|
||||||
|
• Feature 3
|
||||||
|
|
||||||
|
I'd love to send you a review key. Are you interested?
|
||||||
|
|
||||||
|
Trailer: [link]
|
||||||
|
Press kit: [link]
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
[Your name]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Launch
|
||||||
|
|
||||||
|
### Monitor Launch
|
||||||
|
|
||||||
|
**First 24 Hours:**
|
||||||
|
- Watch for critical bugs
|
||||||
|
- Monitor social media mentions
|
||||||
|
- Respond to player feedback
|
||||||
|
- Track analytics (sales, downloads, engagement)
|
||||||
|
- Be ready to deploy hotfix if needed
|
||||||
|
|
||||||
|
**First Week:**
|
||||||
|
- Gather reviews and feedback
|
||||||
|
- Plan first update
|
||||||
|
- Engage with community
|
||||||
|
- Share player content
|
||||||
|
- Thank supporters
|
||||||
|
|
||||||
|
### Community Management
|
||||||
|
|
||||||
|
**Platforms to Manage:**
|
||||||
|
- Steam discussions
|
||||||
|
- Discord server
|
||||||
|
- Twitter/X mentions
|
||||||
|
- Reddit threads
|
||||||
|
- Email support
|
||||||
|
|
||||||
|
**Response Times:**
|
||||||
|
- Critical bugs: < 4 hours
|
||||||
|
- General support: < 24 hours
|
||||||
|
- Feature requests: Acknowledge within 48 hours
|
||||||
|
|
||||||
|
### Analytics Review
|
||||||
|
|
||||||
|
**Key Metrics:**
|
||||||
|
- Daily active users (DAU)
|
||||||
|
- Retention (Day 1, 7, 30)
|
||||||
|
- Average session length
|
||||||
|
- Completion rates
|
||||||
|
- Crash rate
|
||||||
|
- Purchase conversion (if applicable)
|
||||||
|
|
||||||
|
See [ANALYTICS_TUTORIAL.md](tutorials/ANALYTICS_TUTORIAL.md) for implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updates and Patches
|
||||||
|
|
||||||
|
### Hotfix (Critical Bugs)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix bug
|
||||||
|
# Test thoroughly
|
||||||
|
# Export new build
|
||||||
|
# Upload to platforms
|
||||||
|
|
||||||
|
# Version: 1.0.0 → 1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update Quickly:**
|
||||||
|
- Web: Instant (just replace files)
|
||||||
|
- itch.io: Upload new build (instant)
|
||||||
|
- Steam: Upload via SteamPipe (< 1 hour)
|
||||||
|
- Mobile: 1-7 day review
|
||||||
|
|
||||||
|
### Content Updates
|
||||||
|
|
||||||
|
**Planning:**
|
||||||
|
- Based on player feedback
|
||||||
|
- Fix common pain points
|
||||||
|
- Add requested features
|
||||||
|
- Balance tweaks
|
||||||
|
- New content
|
||||||
|
|
||||||
|
**Versioning:**
|
||||||
|
```
|
||||||
|
Major.Minor.Patch
|
||||||
|
1.0.0 → 1.1.0 (feature update)
|
||||||
|
1.1.0 → 1.1.1 (bug fix)
|
||||||
|
1.1.1 → 2.0.0 (major update)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changelog:**
|
||||||
|
```
|
||||||
|
Version 1.1.0 - November 15, 2024
|
||||||
|
|
||||||
|
NEW:
|
||||||
|
• New boss battle
|
||||||
|
• 5 new weapons
|
||||||
|
• Photo mode
|
||||||
|
|
||||||
|
IMPROVED:
|
||||||
|
• Better controller support
|
||||||
|
• Faster loading times
|
||||||
|
• Updated UI
|
||||||
|
|
||||||
|
FIXED:
|
||||||
|
• Player getting stuck in walls
|
||||||
|
• Audio crackling issue
|
||||||
|
• Save corruption bug
|
||||||
|
```
|
||||||
|
|
||||||
|
### DLC/Expansions
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Check DLC ownership
|
||||||
|
if AeThexAuth.has_dlc("expansion_pack_1"):
|
||||||
|
enable_expansion_content()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monetization Strategies
|
||||||
|
|
||||||
|
### Premium (Paid)
|
||||||
|
|
||||||
|
**Pricing:**
|
||||||
|
- Research similar games
|
||||||
|
- Consider your costs
|
||||||
|
- Regional pricing
|
||||||
|
- Sales strategy
|
||||||
|
|
||||||
|
**Steam Pricing Tips:**
|
||||||
|
- $9.99 - $19.99: Indie game sweet spot
|
||||||
|
- Avoid $14.99 (psychological barrier)
|
||||||
|
- Plan for sales (20-50% off)
|
||||||
|
|
||||||
|
### Free-to-Play
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
- Game must be fun without paying
|
||||||
|
- No pay-to-win mechanics
|
||||||
|
- Cosmetic items work well
|
||||||
|
- Battle pass model
|
||||||
|
- Generous with free currency
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# In-app purchases
|
||||||
|
func purchase_item(item_id: String):
|
||||||
|
var result = await AeThexAuth.purchase_item(item_id)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
grant_item(item_id)
|
||||||
|
AeThexAnalytics.track_event("iap_purchase", {
|
||||||
|
"item_id": item_id,
|
||||||
|
"price": result.price
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Freemium
|
||||||
|
|
||||||
|
- Free demo/trial
|
||||||
|
- Upgrade to full version
|
||||||
|
- Good for single-player games
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Marketing Channels
|
||||||
|
|
||||||
|
### Social Media
|
||||||
|
|
||||||
|
**Twitter/X:**
|
||||||
|
- Post development updates
|
||||||
|
- Share GIFs of gameplay
|
||||||
|
- Engage with gamedev community
|
||||||
|
- Use hashtags: #indiegame #gamedev
|
||||||
|
|
||||||
|
**TikTok/Instagram:**
|
||||||
|
- Short gameplay clips
|
||||||
|
- Behind-the-scenes
|
||||||
|
- Game development tips
|
||||||
|
- Quick wins/satisfying moments
|
||||||
|
|
||||||
|
**Reddit:**
|
||||||
|
- r/IndieGaming
|
||||||
|
- r/gamedev
|
||||||
|
- Genre-specific subreddits
|
||||||
|
- Avoid spam, engage authentically
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
|
||||||
|
Create a community server:
|
||||||
|
- Announcements channel
|
||||||
|
- General chat
|
||||||
|
- Bug reports
|
||||||
|
- Suggestions
|
||||||
|
- Development updates
|
||||||
|
|
||||||
|
### Email List
|
||||||
|
|
||||||
|
**Build Pre-Launch:**
|
||||||
|
- Capture emails on landing page
|
||||||
|
- Send updates during development
|
||||||
|
- Offer beta access
|
||||||
|
- Announce launch date
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- Mailchimp (free tier)
|
||||||
|
- ConvertKit
|
||||||
|
- Substack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### ❌ Avoid These Mistakes:
|
||||||
|
|
||||||
|
1. **Launching too early** - Polish matters
|
||||||
|
2. **No marketing** - "Build it and they'll come" is a myth
|
||||||
|
3. **Ignoring feedback** - Players know what's not fun
|
||||||
|
4. **No community** - Build audience before launch
|
||||||
|
5. **Poor store page** - First impression is everything
|
||||||
|
6. **Broken multiplayer** - Test with real players
|
||||||
|
7. **No analytics** - You need data to improve
|
||||||
|
8. **Giving up quickly** - Games can have long tails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- **Analytics:** [AeThex Analytics](https://studio.aethex.io/analytics)
|
||||||
|
- **Marketing:** [Presskit()](https://dopresskit.com/)
|
||||||
|
- **Community:** [Discord](https://discord.com)
|
||||||
|
- **Email:** [Mailchimp](https://mailchimp.com)
|
||||||
|
|
||||||
|
### Learning
|
||||||
|
|
||||||
|
- **Podcast:** How to Market a Game Podcast
|
||||||
|
- **Book:** "The Indie Game Developer Handbook"
|
||||||
|
- **YouTube:** Game Marketing channels
|
||||||
|
- **Community:** [r/gamedev](https://reddit.com/r/gamedev)
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
- **Email:** [support@aethex.io](mailto:support@aethex.io)
|
||||||
|
- **Discord:** [AeThex Community](https://discord.gg/aethex)
|
||||||
|
- **Docs:** [docs.aethex.io](https://docs.aethex.io)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist for Launch Day
|
||||||
|
|
||||||
|
**24 Hours Before:**
|
||||||
|
- [ ] Final build tested on all platforms
|
||||||
|
- [ ] Store pages reviewed (no typos!)
|
||||||
|
- [ ] Press emails sent
|
||||||
|
- [ ] Social media posts scheduled
|
||||||
|
- [ ] Discord announcement prepared
|
||||||
|
- [ ] Support email ready to monitor
|
||||||
|
- [ ] Analytics dashboard configured
|
||||||
|
- [ ] Backup plan for critical bugs
|
||||||
|
|
||||||
|
**Launch Day:**
|
||||||
|
- [ ] Publish on all platforms
|
||||||
|
- [ ] Post to social media
|
||||||
|
- [ ] Send email to mailing list
|
||||||
|
- [ ] Post in relevant communities
|
||||||
|
- [ ] Monitor for issues
|
||||||
|
- [ ] Respond to comments
|
||||||
|
- [ ] Thank supporters
|
||||||
|
- [ ] Celebrate! 🎉
|
||||||
|
|
||||||
|
**Week After:**
|
||||||
|
- [ ] Gather feedback
|
||||||
|
- [ ] Plan first update
|
||||||
|
- [ ] Thank press/influencers who covered
|
||||||
|
- [ ] Post-mortem analysis
|
||||||
|
- [ ] Start work on updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
You've learned how to:
|
||||||
|
✅ Prepare your game for launch
|
||||||
|
✅ Publish to multiple platforms
|
||||||
|
✅ Create marketing materials
|
||||||
|
✅ Build and engage community
|
||||||
|
✅ Plan your launch strategy
|
||||||
|
✅ Support your game post-launch
|
||||||
|
✅ Handle updates and patches
|
||||||
|
|
||||||
|
Publishing is just the beginning - support your game and community for long-term success!
|
||||||
|
|
||||||
|
**Need More Help?**
|
||||||
|
- [Export Guide](EXPORTING_GAMES.md) - Technical export details
|
||||||
|
- [Analytics Tutorial](tutorials/ANALYTICS_TUTORIAL.md) - Track your success
|
||||||
|
- [API Reference](API_REFERENCE.md) - Cloud features documentation
|
||||||
|
|
||||||
|
**Good luck with your launch!** 🚀
|
||||||
|
|
@ -139,52 +139,29 @@ Welcome to AeThex Engine - the cloud-first game engine that makes multiplayer, c
|
||||||
|
|
||||||
### Game Development
|
### Game Development
|
||||||
|
|
||||||
- **Basics:**
|
**→ [Complete Game Development Guide](GAME_DEVELOPMENT.md)**
|
||||||
- [GDScript Basics](GDSCRIPT_BASICS.md)
|
|
||||||
- [First Game Tutorial](tutorials/FIRST_GAME_TUTORIAL.md)
|
|
||||||
- Scene system
|
|
||||||
- Node hierarchy
|
|
||||||
- Signals and callbacks
|
|
||||||
|
|
||||||
- **Physics:**
|
Learn all core engine concepts:
|
||||||
- RigidBody2D/3D
|
|
||||||
- StaticBody2D/3D
|
|
||||||
- Collision shapes
|
|
||||||
- Physics layers
|
|
||||||
|
|
||||||
- **UI:**
|
- **[Scene System](GAME_DEVELOPMENT.md#scene-system)** - Building blocks of your game
|
||||||
- Control nodes
|
- **[Node Hierarchy](GAME_DEVELOPMENT.md#node-hierarchy)** - Organizing game objects
|
||||||
- Layouts and containers
|
- **[Signals & Callbacks](GAME_DEVELOPMENT.md#signals-and-callbacks)** - Event-driven programming
|
||||||
- Themes
|
- **[Physics System](GAME_DEVELOPMENT.md#physics)** - 2D/3D physics, collisions, layers
|
||||||
- Responsive design
|
- **[UI System](GAME_DEVELOPMENT.md#ui-system)** - Control nodes, layouts, themes
|
||||||
|
- **[Audio System](GAME_DEVELOPMENT.md#audio-system)** - Music, sound effects, 3D audio
|
||||||
|
- **[Workflow & Tools](GAME_DEVELOPMENT.md#workflow--tools)** - Studio IDE, version control
|
||||||
|
|
||||||
- **Audio:**
|
### Platform Export
|
||||||
- AudioStreamPlayer
|
|
||||||
- Music management
|
|
||||||
- Sound effects
|
|
||||||
- 3D audio
|
|
||||||
|
|
||||||
### Workflow
|
**→ [Complete Export Guide](EXPORTING_GAMES.md)**
|
||||||
|
|
||||||
- **Studio IDE:**
|
Export to all platforms:
|
||||||
- [Studio Integration](STUDIO_INTEGRATION.md)
|
|
||||||
- Code editor
|
|
||||||
- Scene tree
|
|
||||||
- Asset browser
|
|
||||||
- Live reload
|
|
||||||
|
|
||||||
- **Version Control:**
|
- **[Windows Export](EXPORTING_GAMES.md#windows-export)** - Desktop Windows builds, code signing, distribution
|
||||||
- Git integration
|
- **[Linux Export](EXPORTING_GAMES.md#linux-export)** - Linux builds, AppImage, Flatpak, Snap
|
||||||
- Collaboration
|
- **[macOS Export](EXPORTING_GAMES.md#macos-export)** - macOS builds, notarization, App Store
|
||||||
- Branching strategy
|
- **[Web Export](EXPORTING_GAMES.md#web-html5-export)** - HTML5/WebAssembly, PWA, hosting
|
||||||
- Merge conflicts
|
- **[Android Export](EXPORTING_GAMES.md#android-export)** - APK/AAB builds, Play Store, optimization
|
||||||
|
|
||||||
- **Export:**
|
|
||||||
- Windows export
|
|
||||||
- Linux export
|
|
||||||
- macOS export
|
|
||||||
- Web (HTML5) export
|
|
||||||
- Android export
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,53 @@
|
||||||
* [GDScript Basics](GDSCRIPT_BASICS.md)
|
* [GDScript Basics](GDSCRIPT_BASICS.md)
|
||||||
* [First Game Tutorial](tutorials/FIRST_GAME_TUTORIAL.md)
|
* [First Game Tutorial](tutorials/FIRST_GAME_TUTORIAL.md)
|
||||||
|
|
||||||
|
* Game Development
|
||||||
|
* [Complete Guide](GAME_DEVELOPMENT.md)
|
||||||
|
* [Scene System](GAME_DEVELOPMENT.md#scene-system)
|
||||||
|
* [Node Hierarchy](GAME_DEVELOPMENT.md#node-hierarchy)
|
||||||
|
* [Signals & Callbacks](GAME_DEVELOPMENT.md#signals-and-callbacks)
|
||||||
|
* [Physics System](GAME_DEVELOPMENT.md#physics)
|
||||||
|
* [UI System](GAME_DEVELOPMENT.md#ui-system)
|
||||||
|
* [Audio System](GAME_DEVELOPMENT.md#audio-system)
|
||||||
|
* [Workflow & Tools](GAME_DEVELOPMENT.md#workflow--tools)
|
||||||
|
|
||||||
* Tutorials
|
* Tutorials
|
||||||
* [Tutorial Index](tutorials/README.md)
|
* [Tutorial Index](tutorials/README.md)
|
||||||
* [Multiplayer Pong](tutorials/FIRST_GAME_TUTORIAL.md)
|
* [Multiplayer Pong](tutorials/FIRST_GAME_TUTORIAL.md)
|
||||||
|
* [AI Assistant](tutorials/AI_ASSISTANT_TUTORIAL.md)
|
||||||
|
* [Authentication](tutorials/AUTH_TUTORIAL.md)
|
||||||
|
* [Analytics](tutorials/ANALYTICS_TUTORIAL.md)
|
||||||
|
|
||||||
* API Reference
|
* API Reference
|
||||||
* [Complete API Reference](API_REFERENCE.md)
|
* [Complete API Reference](API_REFERENCE.md)
|
||||||
* AeThexCloud
|
* [AeThexCloud](API_REFERENCE.md#aethexcloud-singleton)
|
||||||
* AeThexAuth
|
* [AeThexAuth](API_REFERENCE.md#aethexauth-singleton)
|
||||||
* AeThexSaves
|
* [AeThexSaves](API_REFERENCE.md#aethexsaves-singleton)
|
||||||
* AeThexMultiplayer
|
* [AeThexMultiplayer](API_REFERENCE.md#aethexmultiplayer-singleton)
|
||||||
* AeThexAnalytics
|
* [AeThexAnalytics](API_REFERENCE.md#aethexanalytics-singleton)
|
||||||
* AeThexAI
|
* [AeThexAI](API_REFERENCE.md#aethexai-singleton)
|
||||||
|
* [AeThexStudio](API_REFERENCE.md#aethexstudio-singleton)
|
||||||
|
|
||||||
* Architecture
|
* Architecture
|
||||||
* [Architecture Overview](ARCHITECTURE_OVERVIEW.md)
|
* [Architecture Overview](ARCHITECTURE_OVERVIEW.md)
|
||||||
* [Cloud Services](CLOUD_SERVICES_ARCHITECTURE.md)
|
* [Cloud Services](CLOUD_SERVICES_ARCHITECTURE.md)
|
||||||
* [Studio Integration](STUDIO_INTEGRATION.md)
|
* [Studio Integration](STUDIO_INTEGRATION.md)
|
||||||
|
|
||||||
|
* Platform Export
|
||||||
|
* [Export Guide](EXPORTING_GAMES.md)
|
||||||
|
* [Windows](EXPORTING_GAMES.md#windows-export)
|
||||||
|
* [Linux](EXPORTING_GAMES.md#linux-export)
|
||||||
|
* [macOS](EXPORTING_GAMES.md#macos-export)
|
||||||
|
* [Web (HTML5)](EXPORTING_GAMES.md#web-html5-export)
|
||||||
|
* [Android](EXPORTING_GAMES.md#android-export)
|
||||||
|
|
||||||
|
* Publishing
|
||||||
|
* [Publishing Guide](PUBLISHING_GUIDE.md)
|
||||||
|
* [Pre-Launch Checklist](PUBLISHING_GUIDE.md#pre-launch-checklist)
|
||||||
|
* [Platform Submission](PUBLISHING_GUIDE.md#platform-specific-publishing)
|
||||||
|
* [Marketing Materials](PUBLISHING_GUIDE.md#marketing-materials)
|
||||||
|
* [Launch Strategy](PUBLISHING_GUIDE.md#launch-strategy)
|
||||||
|
|
||||||
* Developer Guides
|
* Developer Guides
|
||||||
* [Building from Source](BUILDING_WINDOWS.md)
|
* [Building from Source](BUILDING_WINDOWS.md)
|
||||||
* [Studio Bridge Guide](STUDIO_BRIDGE_GUIDE.md)
|
* [Studio Bridge Guide](STUDIO_BRIDGE_GUIDE.md)
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,14 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
<meta name="description" content="AeThex Engine - Cloud-first game engine documentation">
|
<meta name="description" content="AeThex Engine - Cloud-first game engine documentation">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
|
|
||||||
<style>
|
<!-- Fonts -->
|
||||||
:root {
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
--theme-color: #8B5CF6;
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
--theme-color-secondary: #06B6D4;
|
<link href="https://fonts.googleapis.com/css2?family=Electrolize&family=Source+Code+Pro:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
}
|
|
||||||
.app-name-link img {
|
<!-- Custom Cyberpunk Theme -->
|
||||||
width: 40px;
|
<link rel="stylesheet" href="theme.css">
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">Loading...</div>
|
<div id="app">Loading...</div>
|
||||||
|
|
|
||||||
527
docs/theme.css
Normal file
527
docs/theme.css
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
/* AeThex Cyberpunk Documentation Theme */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Electrolize&family=Source+Code+Pro:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Cyberpunk Dark Backgrounds */
|
||||||
|
--base-background-color: #000000;
|
||||||
|
--base-color: #ffffff;
|
||||||
|
|
||||||
|
/* Neon Accents */
|
||||||
|
--theme-color: #00ffff;
|
||||||
|
--theme-color-secondary: #ff00ff;
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #b0b0b0;
|
||||||
|
--text-muted: #808080;
|
||||||
|
|
||||||
|
/* Borders with Glow */
|
||||||
|
--border-primary: rgba(0, 255, 255, 0.3);
|
||||||
|
--border-glow: rgba(0, 255, 255, 0.5);
|
||||||
|
|
||||||
|
/* Code Blocks */
|
||||||
|
--code-theme-background: #0a0a0a;
|
||||||
|
--code-theme-text: #00ffff;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-background: #0a0a0a;
|
||||||
|
--sidebar-border-color: rgba(0, 255, 255, 0.2);
|
||||||
|
|
||||||
|
/* Search */
|
||||||
|
--search-input-background-color: #1a1a1a;
|
||||||
|
--search-input-border-color: rgba(0, 255, 255, 0.3);
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
--link-color: #00ffff;
|
||||||
|
--link-color-hover: #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Styling */
|
||||||
|
* {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Source Code Pro', 'Courier New', monospace;
|
||||||
|
background: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline Effect Overlay */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(0, 255, 255, 0.03) 0px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0, 255, 255, 0.03) 3px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: scanline 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanline {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
100% { transform: translateY(10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings with Neon Glow */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Electrolize', sans-serif;
|
||||||
|
color: #00ffff;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5),
|
||||||
|
0 0 20px rgba(0, 255, 255, 0.3);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
border-bottom: 2px solid rgba(0, 255, 255, 0.3);
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.6em;
|
||||||
|
margin-top: 2em;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2::before {
|
||||||
|
content: '▶';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #ff00ff;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 0, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #ff00ff;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links with Neon Hover */
|
||||||
|
a {
|
||||||
|
color: #00ffff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #ff00ff !important;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 0, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Cyberpunk Style */
|
||||||
|
.sidebar {
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-right: 1px solid rgba(0, 255, 255, 0.2);
|
||||||
|
box-shadow: 2px 0 20px rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav li {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav a {
|
||||||
|
color: #b0b0b0 !important;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
display: block;
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav a:hover {
|
||||||
|
color: #00ffff !important;
|
||||||
|
border-left-color: #00ffff !important;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav li.active > a {
|
||||||
|
color: #00ffff !important;
|
||||||
|
border-left-color: #ff00ff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App Name with Glow */
|
||||||
|
.app-name {
|
||||||
|
font-family: 'Electrolize', sans-serif;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #00ffff;
|
||||||
|
text-shadow: 0 0 20px rgba(0, 255, 255, 0.8),
|
||||||
|
0 0 40px rgba(0, 255, 255, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
padding: 0.8em 1em;
|
||||||
|
border-bottom: 1px solid rgba(0, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo Sizing */
|
||||||
|
.app-name img,
|
||||||
|
.sidebar img,
|
||||||
|
.github-corner,
|
||||||
|
.github-corner svg {
|
||||||
|
max-width: 60px !important;
|
||||||
|
max-height: 60px !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name-link img {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GitHub Corner Repositioning */
|
||||||
|
.github-corner {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
width: 60px !important;
|
||||||
|
height: 60px !important;
|
||||||
|
z-index: 100 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-corner svg {
|
||||||
|
fill: #00ffff !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
width: 60px !important;
|
||||||
|
height: 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-corner:hover svg {
|
||||||
|
fill: #ff00ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-corner .octo-arm,
|
||||||
|
.github-corner .octo-body {
|
||||||
|
fill: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-corner:hover .octo-arm {
|
||||||
|
animation: octocat-wave 560ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes octocat-wave {
|
||||||
|
0%, 100% { transform: rotate(0); }
|
||||||
|
20%, 60% { transform: rotate(-25deg); }
|
||||||
|
40%, 80% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.content {
|
||||||
|
padding: 2em 3em;
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Images */
|
||||||
|
.content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Blocks with Neon Border */
|
||||||
|
pre {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1.5em !important;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.1);
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
color: #00ffff !important;
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline Code */
|
||||||
|
code {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #ff00ff;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(255, 0, 255, 0.3);
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blockquotes with Neon Accent */
|
||||||
|
blockquote {
|
||||||
|
background: #0a0a0a;
|
||||||
|
border-left: 4px solid #00ffff;
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
color: #b0b0b0;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables with Cyberpunk Grid */
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 2em 0;
|
||||||
|
background: #0a0a0a;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-bottom: 2px solid #00ffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
color: #00ffff;
|
||||||
|
font-family: 'Electrolize', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: #0f0f0f;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 0.8em 1em;
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Box */
|
||||||
|
.search input {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 0.8em 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #00ffff;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search input::placeholder {
|
||||||
|
color: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges/Tags */
|
||||||
|
.label {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #00ffff;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
|
padding: 0.2em 0.6em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.docsify-pagination-container {
|
||||||
|
border-top: 1px solid rgba(0, 255, 255, 0.2);
|
||||||
|
margin-top: 3em;
|
||||||
|
padding-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-item-title {
|
||||||
|
font-family: 'Electrolize', sans-serif;
|
||||||
|
color: #00ffff;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-item:hover .pagination-item-title {
|
||||||
|
color: #ff00ff;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkmarks and Lists */
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
position: relative;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li::before {
|
||||||
|
content: '▶';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #00ffff;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox Lists */
|
||||||
|
input[type="checkbox"] {
|
||||||
|
accent-color: #00ffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts/Notices */
|
||||||
|
.tip, .warn, .danger {
|
||||||
|
padding: 1em 1.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
border-left: 4px solid;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
border-left-color: #00ff00;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
border-left-color: #ffff00;
|
||||||
|
box-shadow: 0 0 20px rgba(255, 255, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
border-left-color: #ff0000;
|
||||||
|
box-shadow: 0 0 20px rgba(255, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy Code Button */
|
||||||
|
.docsify-copy-code-button {
|
||||||
|
background: #1a1a1a !important;
|
||||||
|
color: #00ffff !important;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
|
font-family: 'Source Code Pro', monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docsify-copy-code-button:hover {
|
||||||
|
background: #00ffff !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Cyberpunk Style */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #00ffff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glowing Dividers */
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(0, 255, 255, 0.5),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
margin: 3em 0;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Animation */
|
||||||
|
.progress {
|
||||||
|
background: #00ffff;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.content {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-pulse {
|
||||||
|
animation: glow-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Syntax Highlighting Override */
|
||||||
|
.token.comment { color: #505050; }
|
||||||
|
.token.keyword { color: #ff00ff; }
|
||||||
|
.token.string { color: #00ff00; }
|
||||||
|
.token.function { color: #00ffff; }
|
||||||
|
.token.number { color: #ffff00; }
|
||||||
|
.token.operator { color: #ff00ff; }
|
||||||
|
.token.punctuation { color: #808080; }
|
||||||
|
.token.class-name { color: #00ffff; font-weight: bold; }
|
||||||
632
docs/tutorials/AI_ASSISTANT_TUTORIAL.md
Normal file
632
docs/tutorials/AI_ASSISTANT_TUTORIAL.md
Normal file
|
|
@ -0,0 +1,632 @@
|
||||||
|
# AI Assistant Tutorial
|
||||||
|
|
||||||
|
Learn how to integrate AeThex's AI assistant into your game to provide contextual help and coding assistance to players.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You'll Build
|
||||||
|
|
||||||
|
A game with an in-game AI assistant that can:
|
||||||
|
- Answer questions about game mechanics
|
||||||
|
- Provide hints and tips
|
||||||
|
- Generate code snippets
|
||||||
|
- Offer context-aware help
|
||||||
|
|
||||||
|
**Time:** 20 minutes
|
||||||
|
**Difficulty:** Beginner
|
||||||
|
**Prerequisites:** Basic GDScript knowledge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Completed [First Game Tutorial](FIRST_GAME_TUTORIAL.md)
|
||||||
|
- AeThex Cloud connection set up
|
||||||
|
- Basic understanding of UI systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Connect to AI Service
|
||||||
|
|
||||||
|
First, ensure you're connected to AeThex Cloud with AI services enabled.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# main.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Connect to cloud
|
||||||
|
var result = await AeThexCloud.connect_to_cloud()
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print("Connected to AeThex Cloud")
|
||||||
|
else:
|
||||||
|
print("Failed to connect: ", result.error)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Create the AI Assistant UI
|
||||||
|
|
||||||
|
Create a simple chat interface for the AI assistant.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# ai_assistant.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var chat_history = $VBox/ChatHistory
|
||||||
|
@onready var input_field = $VBox/HBox/InputField
|
||||||
|
@onready var send_button = $VBox/HBox/SendButton
|
||||||
|
@onready var loading_indicator = $VBox/LoadingIndicator
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
send_button.pressed.connect(_on_send_pressed)
|
||||||
|
input_field.text_submitted.connect(_on_text_submitted)
|
||||||
|
loading_indicator.visible = false
|
||||||
|
|
||||||
|
func _on_send_pressed():
|
||||||
|
_send_message(input_field.text)
|
||||||
|
|
||||||
|
func _on_text_submitted(text: String):
|
||||||
|
_send_message(text)
|
||||||
|
|
||||||
|
func _send_message(message: String):
|
||||||
|
if message.strip_edges().is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add user message to chat
|
||||||
|
add_message("You", message, Color.CYAN)
|
||||||
|
input_field.clear()
|
||||||
|
|
||||||
|
# Show loading indicator
|
||||||
|
loading_indicator.visible = true
|
||||||
|
send_button.disabled = true
|
||||||
|
|
||||||
|
# Ask AI assistant
|
||||||
|
var response = await AeThexAI.ask_assistant(message)
|
||||||
|
|
||||||
|
# Hide loading indicator
|
||||||
|
loading_indicator.visible = false
|
||||||
|
send_button.disabled = false
|
||||||
|
|
||||||
|
# Add AI response to chat
|
||||||
|
if response.success:
|
||||||
|
add_message("AI Assistant", response.answer, Color.GREEN)
|
||||||
|
else:
|
||||||
|
add_message("AI Assistant", "Sorry, I couldn't process that. " + response.error, Color.RED)
|
||||||
|
|
||||||
|
func add_message(sender: String, text: String, color: Color):
|
||||||
|
var label = Label.new()
|
||||||
|
label.text = "[%s]: %s" % [sender, text]
|
||||||
|
label.modulate = color
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
chat_history.add_child(label)
|
||||||
|
|
||||||
|
# Auto-scroll to bottom
|
||||||
|
await get_tree().process_frame
|
||||||
|
if chat_history.get_v_scroll_bar():
|
||||||
|
chat_history.scroll_vertical = chat_history.get_v_scroll_bar().max_value
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Create the Scene
|
||||||
|
|
||||||
|
Create a scene structure for the AI assistant:
|
||||||
|
|
||||||
|
```
|
||||||
|
AIAssistant (Control)
|
||||||
|
├── Panel (Panel)
|
||||||
|
│ └── VBox (VBoxContainer)
|
||||||
|
│ ├── Title (Label) - "AI Assistant"
|
||||||
|
│ ├── ChatHistory (ScrollContainer)
|
||||||
|
│ │ └── MessageContainer (VBoxContainer)
|
||||||
|
│ ├── LoadingIndicator (Label) - "Thinking..."
|
||||||
|
│ └── HBox (HBoxContainer)
|
||||||
|
│ ├── InputField (LineEdit)
|
||||||
|
│ └── SendButton (Button) - "Send"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Layout Tips:**
|
||||||
|
- Set Panel anchor to center
|
||||||
|
- ChatHistory should expand vertically
|
||||||
|
- InputField should expand horizontally
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Context-Aware Help
|
||||||
|
|
||||||
|
Make the AI assistant aware of the player's current context.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# context_aware_assistant.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var player_context = {
|
||||||
|
"current_level": 1,
|
||||||
|
"player_health": 100,
|
||||||
|
"inventory": [],
|
||||||
|
"last_checkpoint": "start",
|
||||||
|
}
|
||||||
|
|
||||||
|
func ask_with_context(question: String):
|
||||||
|
# Build context string
|
||||||
|
var context = "Player context: Level %d, Health: %d, Checkpoint: %s" % [
|
||||||
|
player_context.current_level,
|
||||||
|
player_context.player_health,
|
||||||
|
player_context.last_checkpoint
|
||||||
|
]
|
||||||
|
|
||||||
|
# Include context in the question
|
||||||
|
var full_question = "%s\n\nContext: %s" % [question, context]
|
||||||
|
|
||||||
|
# Ask AI
|
||||||
|
var response = await AeThexAI.ask_assistant(full_question)
|
||||||
|
return response
|
||||||
|
|
||||||
|
func update_context(key: String, value):
|
||||||
|
player_context[key] = value
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Pre-defined Quick Help
|
||||||
|
|
||||||
|
Add quick help buttons for common questions.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# quick_help.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var assistant = get_node("../AIAssistant")
|
||||||
|
|
||||||
|
var quick_questions = [
|
||||||
|
"How do I jump higher?",
|
||||||
|
"What does this item do?",
|
||||||
|
"Where should I go next?",
|
||||||
|
"How do I defeat this enemy?",
|
||||||
|
]
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Create buttons for quick questions
|
||||||
|
for question in quick_questions:
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = question
|
||||||
|
button.pressed.connect(_on_quick_question.bind(question))
|
||||||
|
add_child(button)
|
||||||
|
|
||||||
|
func _on_quick_question(question: String):
|
||||||
|
assistant.send_message(question)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Code Generation
|
||||||
|
|
||||||
|
Use the AI to generate code snippets for players.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# code_generator.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var code_output = $VBox/CodeOutput
|
||||||
|
@onready var request_input = $VBox/RequestInput
|
||||||
|
@onready var generate_button = $VBox/GenerateButton
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
generate_button.pressed.connect(_on_generate_pressed)
|
||||||
|
|
||||||
|
func _on_generate_pressed():
|
||||||
|
var request = request_input.text
|
||||||
|
|
||||||
|
if request.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Request code generation
|
||||||
|
var prompt = "Generate GDScript code for: " + request
|
||||||
|
var response = await AeThexAI.generate_code(prompt)
|
||||||
|
|
||||||
|
if response.success:
|
||||||
|
code_output.text = response.code
|
||||||
|
# Add syntax highlighting
|
||||||
|
code_output.syntax_highlighter = GDScriptSyntaxHighlighter.new()
|
||||||
|
else:
|
||||||
|
code_output.text = "Error: " + response.error
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Hint System
|
||||||
|
|
||||||
|
Create a progressive hint system using the AI.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# hint_system.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var current_puzzle = "temple_door"
|
||||||
|
var hint_level = 0
|
||||||
|
|
||||||
|
func get_hint():
|
||||||
|
hint_level += 1
|
||||||
|
|
||||||
|
var prompt = "Give hint level %d (out of 3) for puzzle: %s. Be progressively more specific." % [
|
||||||
|
hint_level,
|
||||||
|
current_puzzle
|
||||||
|
]
|
||||||
|
|
||||||
|
var response = await AeThexAI.ask_assistant(prompt)
|
||||||
|
|
||||||
|
if response.success:
|
||||||
|
return response.answer
|
||||||
|
else:
|
||||||
|
return "No hints available"
|
||||||
|
|
||||||
|
func reset_hints():
|
||||||
|
hint_level = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Tutorial Generator
|
||||||
|
|
||||||
|
Let AI generate custom tutorials for players.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tutorial_generator.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func generate_tutorial_for_mechanic(mechanic: String):
|
||||||
|
var prompt = """
|
||||||
|
Create a brief in-game tutorial for the mechanic: %s
|
||||||
|
Format:
|
||||||
|
1. Title
|
||||||
|
2. 3 simple steps
|
||||||
|
3. Tips
|
||||||
|
""" % mechanic
|
||||||
|
|
||||||
|
var response = await AeThexAI.ask_assistant(prompt)
|
||||||
|
|
||||||
|
if response.success:
|
||||||
|
return parse_tutorial(response.answer)
|
||||||
|
else:
|
||||||
|
return null
|
||||||
|
|
||||||
|
func parse_tutorial(text: String) -> Dictionary:
|
||||||
|
# Parse AI response into structured tutorial
|
||||||
|
return {
|
||||||
|
"title": "Tutorial",
|
||||||
|
"steps": text.split("\n"),
|
||||||
|
"completed": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 9: Error Explanation
|
||||||
|
|
||||||
|
Help players understand errors in custom scripting.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# error_explainer.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func explain_error(error_message: String):
|
||||||
|
var prompt = """
|
||||||
|
Explain this game scripting error in simple terms and suggest a fix:
|
||||||
|
|
||||||
|
Error: %s
|
||||||
|
|
||||||
|
Target audience: Beginners
|
||||||
|
""" % error_message
|
||||||
|
|
||||||
|
var response = await AeThexAI.ask_assistant(prompt)
|
||||||
|
|
||||||
|
if response.success:
|
||||||
|
return {
|
||||||
|
"explanation": response.answer,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"explanation": "Could not explain error",
|
||||||
|
"success": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 10: Rate Limiting and Caching
|
||||||
|
|
||||||
|
Implement rate limiting to prevent API abuse.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# ai_manager.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var request_cache = {}
|
||||||
|
var last_request_time = 0
|
||||||
|
var min_request_interval = 2.0 # seconds
|
||||||
|
|
||||||
|
func ask_with_cache(question: String):
|
||||||
|
# Check cache first
|
||||||
|
if question in request_cache:
|
||||||
|
return request_cache[question]
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
var time_since_last = Time.get_ticks_msec() / 1000.0 - last_request_time
|
||||||
|
if time_since_last < min_request_interval:
|
||||||
|
var wait_time = min_request_interval - time_since_last
|
||||||
|
await get_tree().create_timer(wait_time).timeout
|
||||||
|
|
||||||
|
# Make request
|
||||||
|
last_request_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
var response = await AeThexAI.ask_assistant(question)
|
||||||
|
|
||||||
|
# Cache successful responses
|
||||||
|
if response.success:
|
||||||
|
request_cache[question] = response
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
func clear_cache():
|
||||||
|
request_cache.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example: In-Game Help System
|
||||||
|
|
||||||
|
Here's a complete help system implementation:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# help_system.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var help_panel = $HelpPanel
|
||||||
|
@onready var chat_container = $HelpPanel/VBox/ChatContainer
|
||||||
|
@onready var input_field = $HelpPanel/VBox/HBox/Input
|
||||||
|
@onready var send_btn = $HelpPanel/VBox/HBox/SendBtn
|
||||||
|
@onready var quick_help = $HelpPanel/VBox/QuickHelp
|
||||||
|
|
||||||
|
var is_open = false
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
help_panel.visible = false
|
||||||
|
send_btn.pressed.connect(_on_send)
|
||||||
|
input_field.text_submitted.connect(_on_submit)
|
||||||
|
|
||||||
|
# Toggle with F1
|
||||||
|
set_process_input(true)
|
||||||
|
|
||||||
|
# Setup quick help buttons
|
||||||
|
setup_quick_help()
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if event.is_action_pressed("ui_help"): # F1
|
||||||
|
toggle_help()
|
||||||
|
|
||||||
|
func toggle_help():
|
||||||
|
is_open = !is_open
|
||||||
|
help_panel.visible = is_open
|
||||||
|
|
||||||
|
if is_open:
|
||||||
|
input_field.grab_focus()
|
||||||
|
|
||||||
|
func _on_send():
|
||||||
|
_send_message(input_field.text)
|
||||||
|
|
||||||
|
func _on_submit(text: String):
|
||||||
|
_send_message(text)
|
||||||
|
|
||||||
|
func _send_message(message: String):
|
||||||
|
if message.strip_edges().is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
add_message("You", message, Color.CYAN)
|
||||||
|
input_field.clear()
|
||||||
|
|
||||||
|
# Get context from game
|
||||||
|
var context = get_game_context()
|
||||||
|
var full_message = "%s\n\nGame context: %s" % [message, context]
|
||||||
|
|
||||||
|
# Ask AI
|
||||||
|
var response = await AeThexAI.ask_assistant(full_message)
|
||||||
|
|
||||||
|
if response.success:
|
||||||
|
add_message("Assistant", response.answer, Color.GREEN)
|
||||||
|
else:
|
||||||
|
add_message("Assistant", "Error: " + response.error, Color.RED)
|
||||||
|
|
||||||
|
func add_message(sender: String, text: String, color: Color):
|
||||||
|
var msg = RichTextLabel.new()
|
||||||
|
msg.bbcode_enabled = true
|
||||||
|
msg.text = "[color=%s][b]%s:[/b] %s[/color]" % [color.to_html(), sender, text]
|
||||||
|
msg.fit_content = true
|
||||||
|
chat_container.add_child(msg)
|
||||||
|
|
||||||
|
func get_game_context() -> String:
|
||||||
|
# Gather relevant game state
|
||||||
|
var player = get_tree().get_first_node_in_group("player")
|
||||||
|
if player:
|
||||||
|
return "Level: %d, Health: %d, Position: %s" % [
|
||||||
|
player.current_level,
|
||||||
|
player.health,
|
||||||
|
player.global_position
|
||||||
|
]
|
||||||
|
return "No context available"
|
||||||
|
|
||||||
|
func setup_quick_help():
|
||||||
|
var questions = [
|
||||||
|
"How do I play?",
|
||||||
|
"What are the controls?",
|
||||||
|
"Where should I go?",
|
||||||
|
"How do I use items?",
|
||||||
|
]
|
||||||
|
|
||||||
|
for q in questions:
|
||||||
|
var btn = Button.new()
|
||||||
|
btn.text = q
|
||||||
|
btn.pressed.connect(func(): _send_message(q))
|
||||||
|
quick_help.add_child(btn)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. **Provide Context**
|
||||||
|
Always include relevant game state when asking the AI:
|
||||||
|
```gdscript
|
||||||
|
var context = "Player is at checkpoint 3, has 50% health"
|
||||||
|
var question_with_context = "%s\nContext: %s" % [user_question, context]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Clear Expectations**
|
||||||
|
Tell players what the AI can and cannot do:
|
||||||
|
```gdscript
|
||||||
|
var help_text = """
|
||||||
|
AI Assistant can help with:
|
||||||
|
- Game mechanics
|
||||||
|
- Quest objectives
|
||||||
|
- Strategy tips
|
||||||
|
|
||||||
|
Cannot help with:
|
||||||
|
- Technical support
|
||||||
|
- Account issues
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Rate Limiting**
|
||||||
|
Prevent abuse with rate limiting:
|
||||||
|
```gdscript
|
||||||
|
const MAX_REQUESTS_PER_MINUTE = 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Caching**
|
||||||
|
Cache common questions to reduce API calls:
|
||||||
|
```gdscript
|
||||||
|
var faq_cache = {
|
||||||
|
"how to jump": "Press Space or A button",
|
||||||
|
"how to save": "Game auto-saves at checkpoints",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Fallback Responses**
|
||||||
|
Always have fallback responses:
|
||||||
|
```gdscript
|
||||||
|
if not response.success:
|
||||||
|
return "Check the tutorial at Main Menu → Help"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Personality Customization
|
||||||
|
|
||||||
|
Give your AI assistant personality:
|
||||||
|
```gdscript
|
||||||
|
func ask_with_personality(question: String):
|
||||||
|
var system_prompt = """
|
||||||
|
You are a helpful wizard companion in a fantasy RPG.
|
||||||
|
Speak in a wise, mystical tone.
|
||||||
|
Be encouraging and friendly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
var response = await AeThexAI.ask_assistant(
|
||||||
|
question,
|
||||||
|
{"system_prompt": system_prompt}
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Language Support
|
||||||
|
|
||||||
|
Support multiple languages:
|
||||||
|
```gdscript
|
||||||
|
func ask_in_language(question: String, language: String):
|
||||||
|
var prompt = "Answer in %s: %s" % [language, question]
|
||||||
|
return await AeThexAI.ask_assistant(prompt)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voice Integration
|
||||||
|
|
||||||
|
Combine with text-to-speech:
|
||||||
|
```gdscript
|
||||||
|
func speak_response(text: String):
|
||||||
|
# Use DisplayServer TTS if available
|
||||||
|
if DisplayServer.tts_is_speaking():
|
||||||
|
DisplayServer.tts_stop()
|
||||||
|
|
||||||
|
DisplayServer.tts_speak(text, "en", 50, 1.0, 1.0, 0, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test your AI assistant:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# test_ai_assistant.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
run_tests()
|
||||||
|
|
||||||
|
func run_tests():
|
||||||
|
print("Testing AI Assistant...")
|
||||||
|
|
||||||
|
# Test basic question
|
||||||
|
var r1 = await AeThexAI.ask_assistant("How do I move?")
|
||||||
|
assert(r1.success, "Basic question failed")
|
||||||
|
|
||||||
|
# Test code generation
|
||||||
|
var r2 = await AeThexAI.generate_code("Create a jump function")
|
||||||
|
assert(r2.success and "func" in r2.code, "Code generation failed")
|
||||||
|
|
||||||
|
print("All tests passed!")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**AI not responding:**
|
||||||
|
- Check cloud connection: `AeThexCloud.is_connected()`
|
||||||
|
- Verify AI service is enabled in project settings
|
||||||
|
- Check console for error messages
|
||||||
|
|
||||||
|
**Slow responses:**
|
||||||
|
- Implement request caching
|
||||||
|
- Use loading indicators
|
||||||
|
- Consider reducing context size
|
||||||
|
|
||||||
|
**Irrelevant answers:**
|
||||||
|
- Provide more specific context
|
||||||
|
- Use structured prompts
|
||||||
|
- Implement feedback system
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **[Authentication Tutorial](AUTH_TUTORIAL.md)** - Add user accounts
|
||||||
|
- **[Analytics Tutorial](ANALYTICS_TUTORIAL.md)** - Track AI usage
|
||||||
|
- **[API Reference](../API_REFERENCE.md#aethexai-singleton)** - Complete AI API docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
You've learned how to:
|
||||||
|
✅ Integrate AI assistant into your game
|
||||||
|
✅ Create context-aware help systems
|
||||||
|
✅ Generate code and tutorials with AI
|
||||||
|
✅ Implement caching and rate limiting
|
||||||
|
✅ Build an in-game help UI
|
||||||
|
|
||||||
|
The AI assistant can dramatically improve player experience by providing instant, contextual help!
|
||||||
|
|
||||||
|
**Questions?** Ask the AI assistant in your game! 🤖
|
||||||
752
docs/tutorials/ANALYTICS_TUTORIAL.md
Normal file
752
docs/tutorials/ANALYTICS_TUTORIAL.md
Normal file
|
|
@ -0,0 +1,752 @@
|
||||||
|
# Analytics Tutorial
|
||||||
|
|
||||||
|
Learn how to track player behavior, measure engagement, and make data-driven decisions with AeThex Analytics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You'll Build
|
||||||
|
|
||||||
|
A complete analytics system that tracks:
|
||||||
|
- Player events (level complete, item collected, etc.)
|
||||||
|
- User properties (level, skill, preferences)
|
||||||
|
- Custom funnels (onboarding, purchases, retention)
|
||||||
|
- Crash reports and errors
|
||||||
|
- Performance metrics
|
||||||
|
|
||||||
|
**Time:** 25 minutes
|
||||||
|
**Difficulty:** Beginner
|
||||||
|
**Prerequisites:** Basic GDScript knowledge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Use Analytics?
|
||||||
|
|
||||||
|
Analytics helps you:
|
||||||
|
- **Understand players** - See what they do in your game
|
||||||
|
- **Improve retention** - Find where players drop off
|
||||||
|
- **Optimize monetization** - Track purchase funnels
|
||||||
|
- **Fix bugs faster** - Get crash reports automatically
|
||||||
|
- **Make data-driven decisions** - Know what features to build
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Connect to Analytics
|
||||||
|
|
||||||
|
First, connect to AeThex Cloud and initialize analytics:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# main.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Connect to cloud
|
||||||
|
var result = await AeThexCloud.connect_to_cloud()
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print("Connected to AeThex Cloud")
|
||||||
|
initialize_analytics()
|
||||||
|
else:
|
||||||
|
print("Failed to connect: ", result.error)
|
||||||
|
|
||||||
|
func initialize_analytics():
|
||||||
|
# Analytics initialization is automatic
|
||||||
|
# Track app start
|
||||||
|
AeThexAnalytics.track_event("app_started", {
|
||||||
|
"platform": OS.get_name(),
|
||||||
|
"version": ProjectSettings.get_setting("application/config/version")
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Track Basic Events
|
||||||
|
|
||||||
|
Track important game events:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
#player.gd
|
||||||
|
extends CharacterBody2D
|
||||||
|
|
||||||
|
var level = 1
|
||||||
|
var score = 0
|
||||||
|
|
||||||
|
func level_complete():
|
||||||
|
# Track level completion
|
||||||
|
AeThexAnalytics.track_event("level_complete", {
|
||||||
|
"level": level,
|
||||||
|
"score": score,
|
||||||
|
"time_spent": get_level_time(),
|
||||||
|
"deaths": death_count
|
||||||
|
})
|
||||||
|
|
||||||
|
level += 1
|
||||||
|
|
||||||
|
func collect_item(item_name: String):
|
||||||
|
# Track item collection
|
||||||
|
AeThexAnalytics.track_event("item_collected", {
|
||||||
|
"item_name": item_name,
|
||||||
|
"level": level,
|
||||||
|
"timestamp": Time.get_unix_time_from_system()
|
||||||
|
})
|
||||||
|
|
||||||
|
func player_died():
|
||||||
|
# Track deaths
|
||||||
|
AeThexAnalytics.track_event("player_died", {
|
||||||
|
"level": level,
|
||||||
|
"cause": death_cause,
|
||||||
|
"position": global_position
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Set User Properties
|
||||||
|
|
||||||
|
Store persistent information about users:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# analytics_manager.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Set up user properties when game starts
|
||||||
|
setup_user_properties()
|
||||||
|
|
||||||
|
func setup_user_properties():
|
||||||
|
# Basic user info
|
||||||
|
AeThexAnalytics.set_user_property("platform", OS.get_name())
|
||||||
|
AeThexAnalytics.set_user_property("game_version", ProjectSettings.get_setting("application/config/version"))
|
||||||
|
|
||||||
|
# Load from save file
|
||||||
|
var save_data = load_save()
|
||||||
|
if save_data:
|
||||||
|
AeThexAnalytics.set_user_property("player_level", save_data.level)
|
||||||
|
AeThexAnalytics.set_user_property("total_playtime", save_data.playtime)
|
||||||
|
AeThexAnalytics.set_user_property("achievements_unlocked", save_data.achievements.size())
|
||||||
|
|
||||||
|
func on_player_level_up(new_level: int):
|
||||||
|
# Update user property when it changes
|
||||||
|
AeThexAnalytics.set_user_property("player_level", new_level)
|
||||||
|
|
||||||
|
func on_achievement_unlocked(achievement_id: String):
|
||||||
|
# Increment property
|
||||||
|
AeThexAnalytics.increment_user_property("achievements_unlocked", 1)
|
||||||
|
|
||||||
|
# Track event
|
||||||
|
AeThexAnalytics.track_event("achievement_unlocked", {
|
||||||
|
"achievement_id": achievement_id
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Track Screen Views
|
||||||
|
|
||||||
|
Monitor which screens players visit:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# base_screen.gd (inherit from this for all screens)
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var screen_name: String = "Unknown"
|
||||||
|
var screen_enter_time: float = 0
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
screen_enter_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
track_screen_view()
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
track_screen_duration()
|
||||||
|
|
||||||
|
func track_screen_view():
|
||||||
|
AeThexAnalytics.track_event("screen_view", {
|
||||||
|
"screen_name": screen_name,
|
||||||
|
"timestamp": Time.get_unix_time_from_system()
|
||||||
|
})
|
||||||
|
|
||||||
|
func track_screen_duration():
|
||||||
|
var duration = (Time.get_ticks_msec() / 1000.0) - screen_enter_time
|
||||||
|
AeThexAnalytics.track_event("screen_duration", {
|
||||||
|
"screen_name": screen_name,
|
||||||
|
"duration_seconds": duration
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
```gdscript
|
||||||
|
# main_menu.gd
|
||||||
|
extends "res://scripts/base_screen.gd"
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
screen_name = "main_menu"
|
||||||
|
super._ready()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Track Economy Events
|
||||||
|
|
||||||
|
Monitor in-game economy:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# economy_tracker.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func currency_earned(currency_name: String, amount: int, source: String):
|
||||||
|
AeThexAnalytics.track_event("currency_earned", {
|
||||||
|
"currency": currency_name,
|
||||||
|
"amount": amount,
|
||||||
|
"source": source, # "quest", "shop", "reward", etc.
|
||||||
|
"total_balance": get_currency_balance(currency_name)
|
||||||
|
})
|
||||||
|
|
||||||
|
func currency_spent(currency_name: String, amount: int, item: String):
|
||||||
|
AeThexAnalytics.track_event("currency_spent", {
|
||||||
|
"currency": currency_name,
|
||||||
|
"amount": amount,
|
||||||
|
"item": item,
|
||||||
|
"remaining_balance": get_currency_balance(currency_name)
|
||||||
|
})
|
||||||
|
|
||||||
|
func item_purchased(item_id: String, price: int, currency: String):
|
||||||
|
AeThexAnalytics.track_event("item_purchased", {
|
||||||
|
"item_id": item_id,
|
||||||
|
"price": price,
|
||||||
|
"currency": currency,
|
||||||
|
"source": "shop" # or "chest", "reward", etc.
|
||||||
|
})
|
||||||
|
|
||||||
|
func real_money_purchase(product_id: String, price: float, currency: String):
|
||||||
|
AeThexAnalytics.track_event("iap_purchase", {
|
||||||
|
"product_id": product_id,
|
||||||
|
"price": price,
|
||||||
|
"currency": currency,
|
||||||
|
"revenue": price # Important for revenue tracking
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Create Funnels
|
||||||
|
|
||||||
|
Track conversion funnels to understand player flow:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# funnel_tracker.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# Onboarding funnel
|
||||||
|
func track_onboarding_funnel(step: String):
|
||||||
|
var funnel_steps = [
|
||||||
|
"tutorial_start",
|
||||||
|
"tutorial_complete",
|
||||||
|
"first_level_start",
|
||||||
|
"first_level_complete",
|
||||||
|
"account_created"
|
||||||
|
]
|
||||||
|
|
||||||
|
AeThexAnalytics.track_event("onboarding_funnel", {
|
||||||
|
"step": step,
|
||||||
|
"step_number": funnel_steps.find(step) + 1,
|
||||||
|
"total_steps": funnel_steps.size()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Purchase funnel
|
||||||
|
func track_purchase_funnel(step: String, product_id: String = ""):
|
||||||
|
AeThexAnalytics.track_event("purchase_funnel", {
|
||||||
|
"step": step, # "view_shop", "select_item", "confirm", "complete"
|
||||||
|
"product_id": product_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Level progression funnel
|
||||||
|
func track_level_funnel(step: String, level: int):
|
||||||
|
AeThexAnalytics.track_event("level_funnel", {
|
||||||
|
"step": step, # "start", "complete", "fail", "abandon"
|
||||||
|
"level": level
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
```gdscript
|
||||||
|
# tutorial.gd
|
||||||
|
func _ready():
|
||||||
|
FunnelTracker.track_onboarding_funnel("tutorial_start")
|
||||||
|
|
||||||
|
func on_tutorial_complete():
|
||||||
|
FunnelTracker.track_onboarding_funnel("tutorial_complete")
|
||||||
|
|
||||||
|
# shop.gd
|
||||||
|
func _on_item_clicked(product_id):
|
||||||
|
FunnelTracker.track_purchase_funnel("select_item", product_id)
|
||||||
|
|
||||||
|
func _on_purchase_confirmed(product_id):
|
||||||
|
FunnelTracker.track_purchase_funnel("confirm", product_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Track Engagement Metrics
|
||||||
|
|
||||||
|
Measure player engagement:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# engagement_tracker.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var session_start_time: float = 0
|
||||||
|
var total_sessions: int = 0
|
||||||
|
var events_this_session: int = 0
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
start_session()
|
||||||
|
|
||||||
|
func start_session():
|
||||||
|
session_start_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
total_sessions = load_total_sessions() + 1
|
||||||
|
save_total_sessions(total_sessions)
|
||||||
|
|
||||||
|
AeThexAnalytics.track_event("session_start", {
|
||||||
|
"session_number": total_sessions,
|
||||||
|
"days_since_install": get_days_since_install()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set user property
|
||||||
|
AeThexAnalytics.set_user_property("total_sessions", total_sessions)
|
||||||
|
|
||||||
|
func end_session():
|
||||||
|
var session_duration = (Time.get_ticks_msec() / 1000.0) - session_start_time
|
||||||
|
|
||||||
|
AeThexAnalytics.track_event("session_end", {
|
||||||
|
"duration_seconds": session_duration,
|
||||||
|
"events_tracked": events_this_session
|
||||||
|
})
|
||||||
|
|
||||||
|
func _notification(what):
|
||||||
|
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||||
|
end_session()
|
||||||
|
|
||||||
|
func track_engagement_event():
|
||||||
|
events_this_session += 1
|
||||||
|
|
||||||
|
func get_days_since_install() -> int:
|
||||||
|
var install_date = load_install_date()
|
||||||
|
if install_date == 0:
|
||||||
|
install_date = Time.get_unix_time_from_system()
|
||||||
|
save_install_date(install_date)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
var current_time = Time.get_unix_time_from_system()
|
||||||
|
var days = (current_time - install_date) / 86400.0 # seconds in a day
|
||||||
|
return int(days)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Track Errors and Crashes
|
||||||
|
|
||||||
|
Automatically report errors:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# error_tracker.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Catch unhandled errors
|
||||||
|
Engine.get_singleton("ScriptServer").add_global_constant("ErrorTracker", self)
|
||||||
|
|
||||||
|
func track_error(error_message: String, stack_trace: String = ""):
|
||||||
|
AeThexAnalytics.track_event("error_occurred", {
|
||||||
|
"error_message": error_message,
|
||||||
|
"stack_trace": stack_trace,
|
||||||
|
"platform": OS.get_name(),
|
||||||
|
"version": ProjectSettings.get_setting("application/config/version")
|
||||||
|
})
|
||||||
|
|
||||||
|
func track_crash(crash_reason: String):
|
||||||
|
AeThexAnalytics.track_event("game_crash", {
|
||||||
|
"reason": crash_reason,
|
||||||
|
"platform": OS.get_name(),
|
||||||
|
"memory_used": Performance.get_monitor(Performance.MEMORY_STATIC),
|
||||||
|
"fps": Engine.get_frames_per_second()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Catch GDScript errors
|
||||||
|
func _on_error(error_message: String):
|
||||||
|
track_error(error_message, get_stack())
|
||||||
|
|
||||||
|
# Helper to get stack trace
|
||||||
|
func get_stack() -> String:
|
||||||
|
var stack = get_stack_trace()
|
||||||
|
var result = ""
|
||||||
|
for frame in stack:
|
||||||
|
result += "%s:%d in %s()\n" % [frame.source, frame.line, frame.function]
|
||||||
|
return result
|
||||||
|
|
||||||
|
func get_stack_trace() -> Array:
|
||||||
|
return [] # Implemented in debug builds
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 9: Performance Tracking
|
||||||
|
|
||||||
|
Track game performance metrics:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# performance_tracker.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const SAMPLE_INTERVAL = 5.0 # seconds
|
||||||
|
var sample_timer: Timer
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
sample_timer = Timer.new()
|
||||||
|
sample_timer.timeout.connect(_sample_performance)
|
||||||
|
sample_timer.wait_time = SAMPLE_INTERVAL
|
||||||
|
add_child(sample_timer)
|
||||||
|
sample_timer.start()
|
||||||
|
|
||||||
|
func _sample_performance():
|
||||||
|
var fps = Engine.get_frames_per_second()
|
||||||
|
var memory_mb = Performance.get_monitor(Performance.MEMORY_STATIC) / 1024.0 / 1024.0
|
||||||
|
var draw_calls = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
|
||||||
|
|
||||||
|
# Track performance event
|
||||||
|
AeThexAnalytics.track_event("performance_sample", {
|
||||||
|
"fps": fps,
|
||||||
|
"memory_mb": memory_mb,
|
||||||
|
"draw_calls": draw_calls,
|
||||||
|
"scene": get_tree().current_scene.name if get_tree().current_scene else "unknown"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Alert on low performance
|
||||||
|
if fps < 30:
|
||||||
|
track_low_performance(fps, memory_mb)
|
||||||
|
|
||||||
|
func track_low_performance(fps: int, memory_mb: float):
|
||||||
|
AeThexAnalytics.track_event("low_performance", {
|
||||||
|
"fps": fps,
|
||||||
|
"memory_mb": memory_mb,
|
||||||
|
"platform": OS.get_name(),
|
||||||
|
"scene": get_tree().current_scene.name if get_tree().current_scene else "unknown"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 10: A/B Testing
|
||||||
|
|
||||||
|
Implement A/B tests to compare features:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# ab_testing.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var user_variant: String = ""
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
assign_variant()
|
||||||
|
|
||||||
|
func assign_variant():
|
||||||
|
# Check if user already has a variant assigned
|
||||||
|
user_variant = load_user_variant()
|
||||||
|
|
||||||
|
if user_variant.is_empty():
|
||||||
|
# Randomly assign variant (50/50 split)
|
||||||
|
user_variant = "A" if randf() < 0.5 else "B"
|
||||||
|
save_user_variant(user_variant)
|
||||||
|
|
||||||
|
# Set as user property
|
||||||
|
AeThexAnalytics.set_user_property("ab_test_variant", user_variant)
|
||||||
|
|
||||||
|
# Track assignment
|
||||||
|
AeThexAnalytics.track_event("ab_test_assigned", {
|
||||||
|
"variant": user_variant
|
||||||
|
})
|
||||||
|
|
||||||
|
func get_variant() -> String:
|
||||||
|
return user_variant
|
||||||
|
|
||||||
|
func is_variant_a() -> bool:
|
||||||
|
return user_variant == "A"
|
||||||
|
|
||||||
|
func is_variant_b() -> bool:
|
||||||
|
return user_variant == "B"
|
||||||
|
|
||||||
|
func track_conversion(goal: String):
|
||||||
|
AeThexAnalytics.track_event("ab_test_conversion", {
|
||||||
|
"variant": user_variant,
|
||||||
|
"goal": goal
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example usage:**
|
||||||
|
```gdscript
|
||||||
|
# Configure feature based on variant
|
||||||
|
func _ready():
|
||||||
|
if ABTesting.is_variant_a():
|
||||||
|
# Show red button
|
||||||
|
$Button.modulate = Color.RED
|
||||||
|
else:
|
||||||
|
# Show blue button
|
||||||
|
$Button.modulate = Color.BLUE
|
||||||
|
|
||||||
|
func _on_button_pressed():
|
||||||
|
# Track which variant converted
|
||||||
|
ABTesting.track_conversion("button_clicked")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analytics Dashboard
|
||||||
|
|
||||||
|
View your analytics data:
|
||||||
|
|
||||||
|
1. **Go to:** [https://studio.aethex.io/analytics](https://studio.aethex.io/analytics)
|
||||||
|
2. **Select your project**
|
||||||
|
3. **View dashboards:**
|
||||||
|
- Overview: DAU, MAU, retention
|
||||||
|
- Events: All tracked events
|
||||||
|
- Funnels: Conversion funnels
|
||||||
|
- User Properties: Audience segments
|
||||||
|
- Errors: Crash reports
|
||||||
|
- Performance: FPS, memory, load times
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. **Name Events Consistently**
|
||||||
|
```gdscript
|
||||||
|
# ✓ DO - Use snake_case
|
||||||
|
AeThexAnalytics.track_event("level_complete", {})
|
||||||
|
AeThexAnalytics.track_event("item_collected", {})
|
||||||
|
|
||||||
|
# ❌ DON'T - Inconsistent naming
|
||||||
|
AeThexAnalytics.track_event("LevelComplete", {})
|
||||||
|
AeThexAnalytics.track_event("item-collected", {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Include Context in Events**
|
||||||
|
```gdscript
|
||||||
|
# ✓ DO - Rich context
|
||||||
|
AeThexAnalytics.track_event("button_clicked", {
|
||||||
|
"button_name": "play",
|
||||||
|
"screen": "main_menu",
|
||||||
|
"session_time": get_session_time()
|
||||||
|
})
|
||||||
|
|
||||||
|
# ❌ DON'T - No context
|
||||||
|
AeThexAnalytics.track_event("button_clicked", {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Track the User Journey**
|
||||||
|
```gdscript
|
||||||
|
# Track entire player flow
|
||||||
|
AeThexAnalytics.track_event("game_started", {})
|
||||||
|
AeThexAnalytics.track_event("tutorial_started", {})
|
||||||
|
AeThexAnalytics.track_event("tutorial_completed", {})
|
||||||
|
AeThexAnalytics.track_event("first_level_started", {})
|
||||||
|
AeThexAnalytics.track_event("first_level_completed", {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Avoid PII (Personally Identifiable Information)**
|
||||||
|
```gdscript
|
||||||
|
# ❌ DON'T - Include PII
|
||||||
|
AeThexAnalytics.track_event("user_info", {
|
||||||
|
"email": "user@example.com", # Never track emails
|
||||||
|
"name": "John Doe", # Never track real names
|
||||||
|
"ip_address": "192.168.1.1" # Never track IPs
|
||||||
|
})
|
||||||
|
|
||||||
|
# ✓ DO - Use anonymous identifiers
|
||||||
|
AeThexAnalytics.track_event("user_info", {
|
||||||
|
"user_id": "anon_123456",
|
||||||
|
"player_level": 5
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Batch Events if Needed**
|
||||||
|
```gdscript
|
||||||
|
# For high-frequency events, batch them
|
||||||
|
var event_batch = []
|
||||||
|
|
||||||
|
func track_player_movement():
|
||||||
|
event_batch.append({
|
||||||
|
"event": "player_moved",
|
||||||
|
"position": player.position
|
||||||
|
})
|
||||||
|
|
||||||
|
if event_batch.size() >= 10:
|
||||||
|
flush_events()
|
||||||
|
|
||||||
|
func flush_events():
|
||||||
|
for event in event_batch:
|
||||||
|
AeThexAnalytics.track_event(event.event, event)
|
||||||
|
event_batch.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Metrics to Track
|
||||||
|
|
||||||
|
### Engagement Metrics:
|
||||||
|
- Daily Active Users (DAU)
|
||||||
|
- Monthly Active Users (MAU)
|
||||||
|
- Session length
|
||||||
|
- Sessions per user
|
||||||
|
- Day 1/7/30 retention
|
||||||
|
|
||||||
|
### Monetization Metrics:
|
||||||
|
- Average Revenue Per User (ARPU)
|
||||||
|
- Paying User Rate
|
||||||
|
- Lifetime Value (LTV)
|
||||||
|
- Conversion rate
|
||||||
|
|
||||||
|
### Game Metrics:
|
||||||
|
- Level completion rate
|
||||||
|
- Tutorial completion rate
|
||||||
|
- Time to first action
|
||||||
|
- Churn points
|
||||||
|
- Player progression
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Complete Analytics Setup
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# analytics_complete.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal analytics_ready
|
||||||
|
|
||||||
|
var is_initialized = false
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
func initialize():
|
||||||
|
# Wait for cloud connection
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
|
||||||
|
if not AeThexCloud.is_connected():
|
||||||
|
await AeThexCloud.connect_to_cloud()
|
||||||
|
|
||||||
|
# Set up user properties
|
||||||
|
setup_user_properties()
|
||||||
|
|
||||||
|
# Start session tracking
|
||||||
|
start_session()
|
||||||
|
|
||||||
|
# Connect to game signals
|
||||||
|
connect_game_signals()
|
||||||
|
|
||||||
|
is_initialized = true
|
||||||
|
analytics_ready.emit()
|
||||||
|
|
||||||
|
func setup_user_properties():
|
||||||
|
AeThexAnalytics.set_user_property("platform", OS.get_name())
|
||||||
|
AeThexAnalytics.set_user_property("game_version", get_game_version())
|
||||||
|
AeThexAnalytics.set_user_property("install_date", get_install_date())
|
||||||
|
|
||||||
|
func start_session():
|
||||||
|
AeThexAnalytics.track_event("session_start", {
|
||||||
|
"platform": OS.get_name(),
|
||||||
|
"version": get_game_version()
|
||||||
|
})
|
||||||
|
|
||||||
|
func connect_game_signals():
|
||||||
|
# Connect to various game events
|
||||||
|
var game = get_tree().root.get_node("Game")
|
||||||
|
if game:
|
||||||
|
game.level_completed.connect(on_level_completed)
|
||||||
|
game.player_died.connect(on_player_died)
|
||||||
|
game.item_collected.connect(on_item_collected)
|
||||||
|
|
||||||
|
func on_level_completed(level: int, score: int):
|
||||||
|
AeThexAnalytics.track_event("level_complete", {
|
||||||
|
"level": level,
|
||||||
|
"score": score
|
||||||
|
})
|
||||||
|
|
||||||
|
func on_player_died(level: int, cause: String):
|
||||||
|
AeThexAnalytics.track_event("player_died", {
|
||||||
|
"level": level,
|
||||||
|
"cause": cause
|
||||||
|
})
|
||||||
|
|
||||||
|
func on_item_collected(item: String):
|
||||||
|
AeThexAnalytics.track_event("item_collected", {
|
||||||
|
"item": item
|
||||||
|
})
|
||||||
|
|
||||||
|
func get_game_version() -> String:
|
||||||
|
return ProjectSettings.get_setting("application/config/version", "1.0.0")
|
||||||
|
|
||||||
|
func get_install_date() -> int:
|
||||||
|
# Load from save or set to current time
|
||||||
|
var save_file = FileAccess.open("user://install_date.save", FileAccess.READ)
|
||||||
|
if save_file:
|
||||||
|
var date = save_file.get_64()
|
||||||
|
save_file.close()
|
||||||
|
return date
|
||||||
|
else:
|
||||||
|
var date = Time.get_unix_time_from_system()
|
||||||
|
save_file = FileAccess.open("user://install_date.save", FileAccess.WRITE)
|
||||||
|
save_file.store_64(date)
|
||||||
|
save_file.close()
|
||||||
|
return date
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Analytics
|
||||||
|
|
||||||
|
Test your analytics implementation:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# test_analytics.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
test_analytics()
|
||||||
|
|
||||||
|
func test_analytics():
|
||||||
|
print("Testing analytics...")
|
||||||
|
|
||||||
|
# Test basic event
|
||||||
|
AeThexAnalytics.track_event("test_event", {
|
||||||
|
"test": true
|
||||||
|
})
|
||||||
|
|
||||||
|
# Test user property
|
||||||
|
AeThexAnalytics.set_user_property("test_property", "test_value")
|
||||||
|
|
||||||
|
# Check console for confirmation
|
||||||
|
print("Analytics test complete. Check dashboard for events.")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **[Publishing Guide](../PUBLISHING_GUIDE.md)** - Deploy your game with analytics
|
||||||
|
- **[API Reference](../API_REFERENCE.md#aethexanalytics-singleton)** - Complete analytics API
|
||||||
|
- **Dashboard:** View your data at [studio.aethex.io/analytics](https://studio.aethex.io/analytics)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
You've learned how to:
|
||||||
|
✅ Track custom events
|
||||||
|
✅ Set user properties
|
||||||
|
✅ Create conversion funnels
|
||||||
|
✅ Monitor performance
|
||||||
|
✅ Track errors and crashes
|
||||||
|
✅ Implement A/B testing
|
||||||
|
✅ Measure engagement metrics
|
||||||
|
|
||||||
|
Analytics gives you the insights to make your game better and understand your players!
|
||||||
|
|
||||||
|
**Ready to publish?** Check out the [Publishing Guide](../PUBLISHING_GUIDE.md)! 🚀
|
||||||
792
docs/tutorials/AUTH_TUTORIAL.md
Normal file
792
docs/tutorials/AUTH_TUTORIAL.md
Normal file
|
|
@ -0,0 +1,792 @@
|
||||||
|
# Authentication Tutorial
|
||||||
|
|
||||||
|
Learn how to add user authentication to your AeThex game with email/password, OAuth, and guest login support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You'll Build
|
||||||
|
|
||||||
|
A complete authentication system with:
|
||||||
|
- Email/password registration and login
|
||||||
|
- OAuth login (Google, GitHub, Discord)
|
||||||
|
- Guest accounts
|
||||||
|
- User profiles
|
||||||
|
- Session management
|
||||||
|
|
||||||
|
**Time:** 30 minutes
|
||||||
|
**Difficulty:** Beginner
|
||||||
|
**Prerequisites:** Basic GDScript knowledge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Add Authentication?
|
||||||
|
|
||||||
|
Authentication enables:
|
||||||
|
- **Cloud saves** - Save progress across devices
|
||||||
|
- **Multiplayer** - Identify players in matches
|
||||||
|
- **Social features** - Friends, leaderboards, chat
|
||||||
|
- **Analytics** - Track user behavior
|
||||||
|
- **Monetization** - In-app purchases, subscriptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Connect to AeThex Cloud
|
||||||
|
|
||||||
|
First, ensure cloud services are connected:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# main.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Connect to AeThex Cloud
|
||||||
|
var result = await AeThexCloud.connect_to_cloud()
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print("Connected to AeThex Cloud")
|
||||||
|
check_existing_session()
|
||||||
|
else:
|
||||||
|
print("Failed to connect: ", result.error)
|
||||||
|
show_error("Could not connect to servers")
|
||||||
|
|
||||||
|
func check_existing_session():
|
||||||
|
if AeThexAuth.is_logged_in():
|
||||||
|
var user = AeThexAuth.get_current_user()
|
||||||
|
print("Welcome back, ", user.display_name)
|
||||||
|
go_to_main_menu()
|
||||||
|
else:
|
||||||
|
show_login_screen()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Create Login UI
|
||||||
|
|
||||||
|
Create a login screen with options for different auth methods:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# login_screen.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var email_field = $VBox/EmailField
|
||||||
|
@onready var password_field = $VBox/PasswordField
|
||||||
|
@onready var login_btn = $VBox/LoginButton
|
||||||
|
@onready var register_btn = $VBox/RegisterButton
|
||||||
|
@onready var guest_btn = $VBox/GuestButton
|
||||||
|
@onready var google_btn = $VBox/OAuthButtons/GoogleButton
|
||||||
|
@onready var github_btn = $VBox/OAuthButtons/GitHubButton
|
||||||
|
@onready var discord_btn = $VBox/OAuthButtons/DiscordButton
|
||||||
|
@onready var status_label = $VBox/StatusLabel
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
login_btn.pressed.connect(_on_login_pressed)
|
||||||
|
register_btn.pressed.connect(_on_register_pressed)
|
||||||
|
guest_btn.pressed.connect(_on_guest_pressed)
|
||||||
|
google_btn.pressed.connect(_on_oauth_pressed.bind("google"))
|
||||||
|
github_btn.pressed.connect(_on_oauth_pressed.bind("github"))
|
||||||
|
discord_btn.pressed.connect(_on_oauth_pressed.bind("discord"))
|
||||||
|
|
||||||
|
func _on_login_pressed():
|
||||||
|
var email = email_field.text
|
||||||
|
var password = password_field.text
|
||||||
|
|
||||||
|
if not validate_email(email):
|
||||||
|
show_status("Invalid email address", Color.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
if password.length() < 6:
|
||||||
|
show_status("Password must be at least 6 characters", Color.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
show_status("Logging in...", Color.YELLOW)
|
||||||
|
set_buttons_enabled(false)
|
||||||
|
|
||||||
|
var result = await AeThexAuth.login_email(email, password)
|
||||||
|
|
||||||
|
set_buttons_enabled(true)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_status("Login successful!", Color.GREEN)
|
||||||
|
on_login_success()
|
||||||
|
else:
|
||||||
|
show_status("Login failed: " + result.error, Color.RED)
|
||||||
|
|
||||||
|
func _on_register_pressed():
|
||||||
|
var email = email_field.text
|
||||||
|
var password = password_field.text
|
||||||
|
|
||||||
|
if not validate_email(email):
|
||||||
|
show_status("Invalid email address", Color.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
if password.length() < 6:
|
||||||
|
show_status("Password must be at least 6 characters", Color.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
show_status("Creating account...", Color.YELLOW)
|
||||||
|
set_buttons_enabled(false)
|
||||||
|
|
||||||
|
var result = await AeThexAuth.register_email(email, password)
|
||||||
|
|
||||||
|
set_buttons_enabled(true)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_status("Account created! Logging in...", Color.GREEN)
|
||||||
|
on_login_success()
|
||||||
|
else:
|
||||||
|
show_status("Registration failed: " + result.error, Color.RED)
|
||||||
|
|
||||||
|
func _on_guest_pressed():
|
||||||
|
show_status("Creating guest account...", Color.YELLOW)
|
||||||
|
set_buttons_enabled(false)
|
||||||
|
|
||||||
|
var result = await AeThexAuth.login_as_guest()
|
||||||
|
|
||||||
|
set_buttons_enabled(true)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_status("Logged in as guest!", Color.GREEN)
|
||||||
|
on_login_success()
|
||||||
|
else:
|
||||||
|
show_status("Guest login failed: " + result.error, Color.RED)
|
||||||
|
|
||||||
|
func _on_oauth_pressed(provider: String):
|
||||||
|
show_status("Opening " + provider + " login...", Color.YELLOW)
|
||||||
|
set_buttons_enabled(false)
|
||||||
|
|
||||||
|
var result = await AeThexAuth.login_oauth(provider)
|
||||||
|
|
||||||
|
set_buttons_enabled(true)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_status("Logged in with " + provider + "!", Color.GREEN)
|
||||||
|
on_login_success()
|
||||||
|
else:
|
||||||
|
show_status("OAuth login failed: " + result.error, Color.RED)
|
||||||
|
|
||||||
|
func validate_email(email: String) -> bool:
|
||||||
|
var regex = RegEx.new()
|
||||||
|
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
||||||
|
return regex.search(email) != null
|
||||||
|
|
||||||
|
func show_status(message: String, color: Color):
|
||||||
|
status_label.text = message
|
||||||
|
status_label.modulate = color
|
||||||
|
|
||||||
|
func set_buttons_enabled(enabled: bool):
|
||||||
|
login_btn.disabled = !enabled
|
||||||
|
register_btn.disabled = !enabled
|
||||||
|
guest_btn.disabled = !enabled
|
||||||
|
google_btn.disabled = !enabled
|
||||||
|
github_btn.disabled = !enabled
|
||||||
|
discord_btn.disabled = !enabled
|
||||||
|
|
||||||
|
func on_login_success():
|
||||||
|
# Transition to main menu
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Create Registration Flow
|
||||||
|
|
||||||
|
Separate registration screen with additional fields:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# registration_screen.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var email_field = $VBox/EmailField
|
||||||
|
@onready var password_field = $VBox/PasswordField
|
||||||
|
@onready var confirm_password_field = $VBox/ConfirmPasswordField
|
||||||
|
@onready var display_name_field = $VBox/DisplayNameField
|
||||||
|
@onready var terms_checkbox = $VBox/TermsCheckbox
|
||||||
|
@onready var register_btn = $VBox/RegisterButton
|
||||||
|
@onready var back_btn = $VBox/BackButton
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
register_btn.pressed.connect(_on_register_pressed)
|
||||||
|
back_btn.pressed.connect(_on_back_pressed)
|
||||||
|
|
||||||
|
func _on_register_pressed():
|
||||||
|
# Validation
|
||||||
|
if not validate_input():
|
||||||
|
return
|
||||||
|
|
||||||
|
var email = email_field.text
|
||||||
|
var password = password_field.text
|
||||||
|
var display_name = display_name_field.text
|
||||||
|
|
||||||
|
register_btn.disabled = true
|
||||||
|
|
||||||
|
# Register with additional profile data
|
||||||
|
var result = await AeThexAuth.register_email(email, password, {
|
||||||
|
"display_name": display_name
|
||||||
|
})
|
||||||
|
|
||||||
|
register_btn.disabled = false
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
# Send verification email (optional)
|
||||||
|
await AeThexAuth.send_verification_email()
|
||||||
|
show_success("Account created! Check your email to verify.")
|
||||||
|
else:
|
||||||
|
show_error("Registration failed: " + result.error)
|
||||||
|
|
||||||
|
func validate_input() -> bool:
|
||||||
|
# Check email
|
||||||
|
if not validate_email(email_field.text):
|
||||||
|
show_error("Invalid email address")
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Check password
|
||||||
|
if password_field.text.length() < 6:
|
||||||
|
show_error("Password must be at least 6 characters")
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Check password match
|
||||||
|
if password_field.text != confirm_password_field.text:
|
||||||
|
show_error("Passwords do not match")
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Check display name
|
||||||
|
if display_name_field.text.strip_edges().is_empty():
|
||||||
|
show_error("Display name is required")
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Check terms
|
||||||
|
if not terms_checkbox.button_pressed:
|
||||||
|
show_error("You must accept the terms of service")
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
func validate_email(email: String) -> bool:
|
||||||
|
var regex = RegEx.new()
|
||||||
|
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
||||||
|
return regex.search(email) != null
|
||||||
|
|
||||||
|
func _on_back_pressed():
|
||||||
|
get_tree().change_scene_to_file("res://scenes/login_screen.tscn")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Handle Authentication State
|
||||||
|
|
||||||
|
Create a global authentication manager:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# autoload: auth_manager.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal login_changed(is_logged_in: bool)
|
||||||
|
signal user_profile_updated(profile: Dictionary)
|
||||||
|
|
||||||
|
var current_user: Dictionary = {}
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Listen for auth state changes
|
||||||
|
AeThexAuth.login_state_changed.connect(_on_login_state_changed)
|
||||||
|
|
||||||
|
func _on_login_state_changed(is_logged_in: bool):
|
||||||
|
login_changed.emit(is_logged_in)
|
||||||
|
|
||||||
|
if is_logged_in:
|
||||||
|
current_user = AeThexAuth.get_current_user()
|
||||||
|
print("User logged in: ", current_user.display_name)
|
||||||
|
else:
|
||||||
|
current_user = {}
|
||||||
|
print("User logged out")
|
||||||
|
|
||||||
|
func is_logged_in() -> bool:
|
||||||
|
return AeThexAuth.is_logged_in()
|
||||||
|
|
||||||
|
func get_user_id() -> String:
|
||||||
|
return current_user.get("user_id", "")
|
||||||
|
|
||||||
|
func get_display_name() -> String:
|
||||||
|
return current_user.get("display_name", "Guest")
|
||||||
|
|
||||||
|
func get_email() -> String:
|
||||||
|
return current_user.get("email", "")
|
||||||
|
|
||||||
|
func is_guest() -> bool:
|
||||||
|
return current_user.get("is_guest", false)
|
||||||
|
|
||||||
|
func logout():
|
||||||
|
await AeThexAuth.logout()
|
||||||
|
get_tree().change_scene_to_file("res://scenes/login_screen.tscn")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to Project Settings:**
|
||||||
|
```
|
||||||
|
Project → Project Settings → Autoload
|
||||||
|
Name: AuthManager
|
||||||
|
Path: res://scripts/auth_manager.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: User Profiles
|
||||||
|
|
||||||
|
Display and edit user profiles:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# profile_screen.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var avatar_texture = $VBox/Avatar
|
||||||
|
@onready var display_name_label = $VBox/DisplayName
|
||||||
|
@onready var email_label = $VBox/Email
|
||||||
|
@onready var user_id_label = $VBox/UserID
|
||||||
|
@onready var edit_btn = $VBox/EditButton
|
||||||
|
@onready var logout_btn = $VBox/LogoutButton
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
edit_btn.pressed.connect(_on_edit_pressed)
|
||||||
|
logout_btn.pressed.connect(_on_logout_pressed)
|
||||||
|
load_profile()
|
||||||
|
|
||||||
|
func load_profile():
|
||||||
|
var user = AeThexAuth.get_current_user()
|
||||||
|
|
||||||
|
display_name_label.text = user.get("display_name", "Unknown")
|
||||||
|
email_label.text = user.get("email", "No email")
|
||||||
|
user_id_label.text = "ID: " + user.get("user_id", "unknown")
|
||||||
|
|
||||||
|
# Load avatar if available
|
||||||
|
if "avatar_url" in user:
|
||||||
|
load_avatar(user.avatar_url)
|
||||||
|
|
||||||
|
func load_avatar(url: String):
|
||||||
|
var http = HTTPRequest.new()
|
||||||
|
add_child(http)
|
||||||
|
http.request_completed.connect(_on_avatar_loaded)
|
||||||
|
http.request(url)
|
||||||
|
|
||||||
|
func _on_avatar_loaded(result, response_code, headers, body):
|
||||||
|
if response_code == 200:
|
||||||
|
var image = Image.new()
|
||||||
|
var error = image.load_png_from_buffer(body)
|
||||||
|
if error == OK:
|
||||||
|
avatar_texture.texture = ImageTexture.create_from_image(image)
|
||||||
|
|
||||||
|
func _on_edit_pressed():
|
||||||
|
get_tree().change_scene_to_file("res://scenes/edit_profile.tscn")
|
||||||
|
|
||||||
|
func _on_logout_pressed():
|
||||||
|
AuthManager.logout()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Edit Profile
|
||||||
|
|
||||||
|
Allow users to update their profile:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# edit_profile.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var display_name_field = $VBox/DisplayNameField
|
||||||
|
@onready var bio_field = $VBox/BioField
|
||||||
|
@onready var save_btn = $VBox/SaveButton
|
||||||
|
@onready var cancel_btn = $VBox/CancelButton
|
||||||
|
|
||||||
|
var original_profile: Dictionary
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
save_btn.pressed.connect(_on_save_pressed)
|
||||||
|
cancel_btn.pressed.connect(_on_cancel_pressed)
|
||||||
|
load_current_profile()
|
||||||
|
|
||||||
|
func load_current_profile():
|
||||||
|
original_profile = AeThexAuth.get_current_user()
|
||||||
|
display_name_field.text = original_profile.get("display_name", "")
|
||||||
|
bio_field.text = original_profile.get("bio", "")
|
||||||
|
|
||||||
|
func _on_save_pressed():
|
||||||
|
var new_profile = {
|
||||||
|
"display_name": display_name_field.text,
|
||||||
|
"bio": bio_field.text
|
||||||
|
}
|
||||||
|
|
||||||
|
save_btn.disabled = true
|
||||||
|
|
||||||
|
var result = await AeThexAuth.update_profile(new_profile)
|
||||||
|
|
||||||
|
save_btn.disabled = false
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
AuthManager.user_profile_updated.emit(new_profile)
|
||||||
|
show_success("Profile updated!")
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
go_back()
|
||||||
|
else:
|
||||||
|
show_error("Failed to update profile: " + result.error)
|
||||||
|
|
||||||
|
func _on_cancel_pressed():
|
||||||
|
go_back()
|
||||||
|
|
||||||
|
func go_back():
|
||||||
|
get_tree().change_scene_to_file("res://scenes/profile_screen.tscn")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Password Reset
|
||||||
|
|
||||||
|
Implement password reset functionality:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# forgot_password.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var email_field = $VBox/EmailField
|
||||||
|
@onready var send_btn = $VBox/SendButton
|
||||||
|
@onready var back_btn = $VBox/BackButton
|
||||||
|
@onready var status_label = $VBox/StatusLabel
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
send_btn.pressed.connect(_on_send_pressed)
|
||||||
|
back_btn.pressed.connect(_on_back_pressed)
|
||||||
|
|
||||||
|
func _on_send_pressed():
|
||||||
|
var email = email_field.text
|
||||||
|
|
||||||
|
if not validate_email(email):
|
||||||
|
show_status("Invalid email address", Color.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
send_btn.disabled = true
|
||||||
|
show_status("Sending reset email...", Color.YELLOW)
|
||||||
|
|
||||||
|
var result = await AeThexAuth.send_password_reset_email(email)
|
||||||
|
|
||||||
|
send_btn.disabled = false
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_status("Reset email sent! Check your inbox.", Color.GREEN)
|
||||||
|
else:
|
||||||
|
show_status("Failed: " + result.error, Color.RED)
|
||||||
|
|
||||||
|
func validate_email(email: String) -> bool:
|
||||||
|
var regex = RegEx.new()
|
||||||
|
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
||||||
|
return regex.search(email) != null
|
||||||
|
|
||||||
|
func _on_back_pressed():
|
||||||
|
get_tree().change_scene_to_file("res://scenes/login_screen.tscn")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Session Management
|
||||||
|
|
||||||
|
Handle session timeouts and refresh:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# session_manager.gd (autoload)
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const SESSION_CHECK_INTERVAL = 300 # 5 minutes
|
||||||
|
const SESSION_TIMEOUT = 3600 # 1 hour
|
||||||
|
|
||||||
|
var session_timer: Timer
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
session_timer = Timer.new()
|
||||||
|
session_timer.timeout.connect(_check_session)
|
||||||
|
session_timer.wait_time = SESSION_CHECK_INTERVAL
|
||||||
|
add_child(session_timer)
|
||||||
|
|
||||||
|
if AuthManager.is_logged_in():
|
||||||
|
session_timer.start()
|
||||||
|
|
||||||
|
func _check_session():
|
||||||
|
if not AeThexAuth.is_logged_in():
|
||||||
|
session_expired()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Refresh session token
|
||||||
|
var result = await AeThexAuth.refresh_session()
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
session_expired()
|
||||||
|
|
||||||
|
func session_expired():
|
||||||
|
session_timer.stop()
|
||||||
|
show_session_expired_dialog()
|
||||||
|
|
||||||
|
func show_session_expired_dialog():
|
||||||
|
var dialog = AcceptDialog.new()
|
||||||
|
dialog.dialog_text = "Your session has expired. Please log in again."
|
||||||
|
dialog.confirmed.connect(func(): get_tree().change_scene_to_file("res://scenes/login_screen.tscn"))
|
||||||
|
get_tree().root.add_child(dialog)
|
||||||
|
dialog.popup_centered()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 9: Account Linking
|
||||||
|
|
||||||
|
Allow users to link multiple auth providers:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# account_linking.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var linked_providers = $VBox/LinkedProviders
|
||||||
|
@onready var available_providers = $VBox/AvailableProviders
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
load_linked_providers()
|
||||||
|
load_available_providers()
|
||||||
|
|
||||||
|
func load_linked_providers():
|
||||||
|
var user = AeThexAuth.get_current_user()
|
||||||
|
var providers = user.get("linked_providers", [])
|
||||||
|
|
||||||
|
for provider in providers:
|
||||||
|
var label = Label.new()
|
||||||
|
label.text = "✓ " + provider.capitalize() + " (linked)"
|
||||||
|
label.modulate = Color.GREEN
|
||||||
|
linked_providers.add_child(label)
|
||||||
|
|
||||||
|
var unlink_btn = Button.new()
|
||||||
|
unlink_btn.text = "Unlink"
|
||||||
|
unlink_btn.pressed.connect(_on_unlink_provider.bind(provider))
|
||||||
|
linked_providers.add_child(unlink_btn)
|
||||||
|
|
||||||
|
func load_available_providers():
|
||||||
|
var all_providers = ["google", "github", "discord", "twitter"]
|
||||||
|
var user = AeThexAuth.get_current_user()
|
||||||
|
var linked = user.get("linked_providers", [])
|
||||||
|
|
||||||
|
for provider in all_providers:
|
||||||
|
if provider not in linked:
|
||||||
|
var btn = Button.new()
|
||||||
|
btn.text = "Link " + provider.capitalize()
|
||||||
|
btn.pressed.connect(_on_link_provider.bind(provider))
|
||||||
|
available_providers.add_child(btn)
|
||||||
|
|
||||||
|
func _on_link_provider(provider: String):
|
||||||
|
var result = await AeThexAuth.link_oauth_provider(provider)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_success("Linked " + provider.capitalize())
|
||||||
|
reload_ui()
|
||||||
|
else:
|
||||||
|
show_error("Failed to link: " + result.error)
|
||||||
|
|
||||||
|
func _on_unlink_provider(provider: String):
|
||||||
|
var result = await AeThexAuth.unlink_provider(provider)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_success("Unlinked " + provider.capitalize())
|
||||||
|
reload_ui()
|
||||||
|
else:
|
||||||
|
show_error("Failed to unlink: " + result.error)
|
||||||
|
|
||||||
|
func reload_ui():
|
||||||
|
# Clear and reload
|
||||||
|
for child in linked_providers.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
for child in available_providers.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
await get_tree().process_frame
|
||||||
|
load_linked_providers()
|
||||||
|
load_available_providers()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 10: Guest Account Conversion
|
||||||
|
|
||||||
|
Allow guests to create permanent accounts:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# guest_conversion.gd
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var email_field = $VBox/EmailField
|
||||||
|
@onready var password_field = $VBox/PasswordField
|
||||||
|
@onready var convert_btn = $VBox/ConvertButton
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
convert_btn.pressed.connect(_on_convert_pressed)
|
||||||
|
|
||||||
|
# Only show if user is guest
|
||||||
|
if not AuthManager.is_guest():
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
func _on_convert_pressed():
|
||||||
|
var email = email_field.text
|
||||||
|
var password = password_field.text
|
||||||
|
|
||||||
|
if not validate_input(email, password):
|
||||||
|
return
|
||||||
|
|
||||||
|
convert_btn.disabled = true
|
||||||
|
|
||||||
|
var result = await AeThexAuth.convert_guest_to_account(email, password)
|
||||||
|
|
||||||
|
convert_btn.disabled = false
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
show_success("Account created! Your progress is saved.")
|
||||||
|
await get_tree().create_timer(2.0).timeout
|
||||||
|
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
|
||||||
|
else:
|
||||||
|
show_error("Conversion failed: " + result.error)
|
||||||
|
|
||||||
|
func validate_input(email: String, password: String) -> bool:
|
||||||
|
if not validate_email(email):
|
||||||
|
show_error("Invalid email address")
|
||||||
|
return false
|
||||||
|
|
||||||
|
if password.length() < 6:
|
||||||
|
show_error("Password must be at least 6 characters")
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
func validate_email(email: String) -> bool:
|
||||||
|
var regex = RegEx.new()
|
||||||
|
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
||||||
|
return regex.search(email) != null
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### 1. **Never Store Passwords**
|
||||||
|
```gdscript
|
||||||
|
# ❌ DON'T
|
||||||
|
var user_password = "secret123" # Never store passwords!
|
||||||
|
|
||||||
|
# ✓ DO
|
||||||
|
# Let AeThexAuth handle authentication securely
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Validate Input**
|
||||||
|
```gdscript
|
||||||
|
func validate_password(password: String) -> bool:
|
||||||
|
if password.length() < 8:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Check for numbers
|
||||||
|
var has_number = false
|
||||||
|
var has_letter = false
|
||||||
|
for c in password:
|
||||||
|
if c.is_valid_int():
|
||||||
|
has_number = true
|
||||||
|
if c.to_lower() >= 'a' and c.to_lower() <= 'z':
|
||||||
|
has_letter = true
|
||||||
|
|
||||||
|
return has_number and has_letter
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Use HTTPS Only**
|
||||||
|
```gdscript
|
||||||
|
# AeThex automatically uses HTTPS
|
||||||
|
# Never disable SSL verification in production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Handle Tokens Securely**
|
||||||
|
```gdscript
|
||||||
|
# Auth tokens are automatically managed by AeThexAuth
|
||||||
|
# Don't try to extract or store them manually
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Rate Limiting**
|
||||||
|
```gdscript
|
||||||
|
var login_attempts = 0
|
||||||
|
var last_attempt_time = 0
|
||||||
|
|
||||||
|
func attempt_login():
|
||||||
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
|
if now - last_attempt_time > 60:
|
||||||
|
login_attempts = 0
|
||||||
|
|
||||||
|
if login_attempts >= 5:
|
||||||
|
show_error("Too many attempts. Try again later.")
|
||||||
|
return
|
||||||
|
|
||||||
|
login_attempts += 1
|
||||||
|
last_attempt_time = now
|
||||||
|
|
||||||
|
# Proceed with login
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test authentication flows:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# test_auth.gd
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
run_tests()
|
||||||
|
|
||||||
|
func run_tests():
|
||||||
|
print("Testing authentication...")
|
||||||
|
|
||||||
|
# Test guest login
|
||||||
|
await test_guest_login()
|
||||||
|
await AeThexAuth.logout()
|
||||||
|
|
||||||
|
# Test email registration
|
||||||
|
await test_email_registration()
|
||||||
|
await AeThexAuth.logout()
|
||||||
|
|
||||||
|
print("All auth tests passed!")
|
||||||
|
|
||||||
|
func test_guest_login():
|
||||||
|
var result = await AeThexAuth.login_as_guest()
|
||||||
|
assert(result.success, "Guest login failed")
|
||||||
|
assert(AeThexAuth.is_logged_in(), "Not logged in after guest login")
|
||||||
|
print("✓ Guest login works")
|
||||||
|
|
||||||
|
func test_email_registration():
|
||||||
|
var test_email = "test%d@example.com" % Time.get_ticks_msec()
|
||||||
|
var test_password = "Test123456"
|
||||||
|
|
||||||
|
var result = await AeThexAuth.register_email(test_email, test_password)
|
||||||
|
assert(result.success, "Registration failed")
|
||||||
|
assert(AeThexAuth.is_logged_in(), "Not logged in after registration")
|
||||||
|
print("✓ Email registration works")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- **[Cloud Saves](FIRST_GAME_TUTORIAL.md#cloud-saves)** - Save user data
|
||||||
|
- **[Analytics Tutorial](ANALYTICS_TUTORIAL.md)** - Track user behavior
|
||||||
|
- **[API Reference](../API_REFERENCE.md#aethexauth-singleton)** - Complete auth API docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
You've learned how to:
|
||||||
|
✅ Implement email/password authentication
|
||||||
|
✅ Add OAuth login (Google, GitHub, Discord)
|
||||||
|
✅ Support guest accounts
|
||||||
|
✅ Manage user profiles
|
||||||
|
✅ Handle password resets
|
||||||
|
✅ Link multiple auth providers
|
||||||
|
✅ Convert guest accounts to permanent accounts
|
||||||
|
|
||||||
|
Authentication is the foundation for cloud features, social systems, and user engagement!
|
||||||
|
|
||||||
|
**Ready to save user data?** Check out [Cloud Saves](FIRST_GAME_TUTORIAL.md#cloud-saves)! 💾
|
||||||
Loading…
Reference in a new issue