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)
|
||||
- [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)
|
||||
|
|
|
|||
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
|
||||
|
||||
- **Basics:**
|
||||
- [GDScript Basics](GDSCRIPT_BASICS.md)
|
||||
- [First Game Tutorial](tutorials/FIRST_GAME_TUTORIAL.md)
|
||||
- Scene system
|
||||
- Node hierarchy
|
||||
- Signals and callbacks
|
||||
**→ [Complete Game Development Guide](GAME_DEVELOPMENT.md)**
|
||||
|
||||
- **Physics:**
|
||||
- RigidBody2D/3D
|
||||
- StaticBody2D/3D
|
||||
- Collision shapes
|
||||
- Physics layers
|
||||
Learn all core engine concepts:
|
||||
|
||||
- **UI:**
|
||||
- Control nodes
|
||||
- Layouts and containers
|
||||
- Themes
|
||||
- Responsive design
|
||||
- **[Scene System](GAME_DEVELOPMENT.md#scene-system)** - Building blocks of your game
|
||||
- **[Node Hierarchy](GAME_DEVELOPMENT.md#node-hierarchy)** - Organizing game objects
|
||||
- **[Signals & Callbacks](GAME_DEVELOPMENT.md#signals-and-callbacks)** - Event-driven programming
|
||||
- **[Physics System](GAME_DEVELOPMENT.md#physics)** - 2D/3D physics, collisions, layers
|
||||
- **[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:**
|
||||
- AudioStreamPlayer
|
||||
- Music management
|
||||
- Sound effects
|
||||
- 3D audio
|
||||
### Platform Export
|
||||
|
||||
### Workflow
|
||||
**→ [Complete Export Guide](EXPORTING_GAMES.md)**
|
||||
|
||||
- **Studio IDE:**
|
||||
- [Studio Integration](STUDIO_INTEGRATION.md)
|
||||
- Code editor
|
||||
- Scene tree
|
||||
- Asset browser
|
||||
- Live reload
|
||||
Export to all platforms:
|
||||
|
||||
- **Version Control:**
|
||||
- Git integration
|
||||
- Collaboration
|
||||
- Branching strategy
|
||||
- Merge conflicts
|
||||
|
||||
- **Export:**
|
||||
- Windows export
|
||||
- Linux export
|
||||
- macOS export
|
||||
- Web (HTML5) export
|
||||
- Android export
|
||||
- **[Windows Export](EXPORTING_GAMES.md#windows-export)** - Desktop Windows builds, code signing, distribution
|
||||
- **[Linux Export](EXPORTING_GAMES.md#linux-export)** - Linux builds, AppImage, Flatpak, Snap
|
||||
- **[macOS Export](EXPORTING_GAMES.md#macos-export)** - macOS builds, notarization, App Store
|
||||
- **[Web Export](EXPORTING_GAMES.md#web-html5-export)** - HTML5/WebAssembly, PWA, hosting
|
||||
- **[Android Export](EXPORTING_GAMES.md#android-export)** - APK/AAB builds, Play Store, optimization
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -7,24 +7,53 @@
|
|||
* [GDScript Basics](GDSCRIPT_BASICS.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
|
||||
* [Tutorial Index](tutorials/README.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
|
||||
* [Complete API Reference](API_REFERENCE.md)
|
||||
* AeThexCloud
|
||||
* AeThexAuth
|
||||
* AeThexSaves
|
||||
* AeThexMultiplayer
|
||||
* AeThexAnalytics
|
||||
* AeThexAI
|
||||
* [AeThexCloud](API_REFERENCE.md#aethexcloud-singleton)
|
||||
* [AeThexAuth](API_REFERENCE.md#aethexauth-singleton)
|
||||
* [AeThexSaves](API_REFERENCE.md#aethexsaves-singleton)
|
||||
* [AeThexMultiplayer](API_REFERENCE.md#aethexmultiplayer-singleton)
|
||||
* [AeThexAnalytics](API_REFERENCE.md#aethexanalytics-singleton)
|
||||
* [AeThexAI](API_REFERENCE.md#aethexai-singleton)
|
||||
* [AeThexStudio](API_REFERENCE.md#aethexstudio-singleton)
|
||||
|
||||
* Architecture
|
||||
* [Architecture Overview](ARCHITECTURE_OVERVIEW.md)
|
||||
* [Cloud Services](CLOUD_SERVICES_ARCHITECTURE.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
|
||||
* [Building from Source](BUILDING_WINDOWS.md)
|
||||
* [Studio Bridge Guide](STUDIO_BRIDGE_GUIDE.md)
|
||||
|
|
|
|||
|
|
@ -6,17 +6,14 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<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">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
|
||||
<style>
|
||||
:root {
|
||||
--theme-color: #8B5CF6;
|
||||
--theme-color-secondary: #06B6D4;
|
||||
}
|
||||
.app-name-link img {
|
||||
width: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Electrolize&family=Source+Code+Pro:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Custom Cyberpunk Theme -->
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<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