@lagless/create 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/pixi-react/AGENTS.md +1 -2
- package/templates/pixi-react/__packageName__-frontend/package.json +3 -2
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +28 -3
- package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +2 -21
- package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +9 -2
- package/templates/pixi-react/docs/08-physics2d.md +53 -39
- package/templates/pixi-react/docs/08-physics3d.md +60 -33
- package/templates/pixi-react/docs/10-common-mistakes.md +4 -9
package/dist/index.js
CHANGED
|
@@ -8,6 +8,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { select } from '@inquirer/prompts';
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
|
+
// Read version from package.json once (single source of truth)
|
|
12
|
+
const _pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
13
|
+
let _packageVersion = '0.0.0';
|
|
14
|
+
try {
|
|
15
|
+
const _pkg = JSON.parse(fs.readFileSync(_pkgJsonPath, 'utf-8'));
|
|
16
|
+
_packageVersion = _pkg.version || _packageVersion;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// fallback
|
|
19
|
+
}
|
|
11
20
|
function toPascalCase(kebab) {
|
|
12
21
|
return kebab.split('-').map((s)=>s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
13
22
|
}
|
|
@@ -60,7 +69,7 @@ async function promptSimulationType() {
|
|
|
60
69
|
]
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
|
-
program.name('create-lagless').description('Scaffold a new Lagless multiplayer game project').version(
|
|
72
|
+
program.name('create-lagless').description('Scaffold a new Lagless multiplayer game project').version(_packageVersion).argument('<project-name>', 'Project name in kebab-case (e.g., my-game)').option('--preset <preset>', 'Project preset', 'pixi-react').option('--port <port>', 'Frontend dev server port', '4203').option('--server-port <port>', 'Backend server port', '3400').option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d').action(async (projectArg, options)=>{
|
|
64
73
|
const targetDir = path.resolve(process.cwd(), projectArg);
|
|
65
74
|
const packageName = path.basename(targetDir).toLowerCase();
|
|
66
75
|
const pascalName = toPascalCase(packageName);
|
|
@@ -90,15 +99,7 @@ program.name('create-lagless').description('Scaffold a new Lagless multiplayer g
|
|
|
90
99
|
} else {
|
|
91
100
|
simulationType = await promptSimulationType();
|
|
92
101
|
}
|
|
93
|
-
|
|
94
|
-
const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');
|
|
95
|
-
let laglessVersion = '0.0.38';
|
|
96
|
-
try {
|
|
97
|
-
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
98
|
-
laglessVersion = pkg.version || laglessVersion;
|
|
99
|
-
} catch (e) {
|
|
100
|
-
// fallback
|
|
101
|
-
}
|
|
102
|
+
const laglessVersion = _packageVersion;
|
|
102
103
|
const vars = {
|
|
103
104
|
projectName: pascalName,
|
|
104
105
|
packageName,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { program } from 'commander';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as ejs from 'ejs';\nimport { execSync } from 'node:child_process';\nimport { fileURLToPath } from 'node:url';\nimport { select } from '@inquirer/prompts';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ntype SimulationType = 'raw' | 'physics2d' | 'physics3d';\n\ninterface CreateOptions {\n preset: string;\n port: string;\n serverPort: string;\n simulationType?: SimulationType;\n}\n\nfunction toPascalCase(kebab: string): string {\n return kebab.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');\n}\n\nfunction getTemplatesDir(): string {\n const devPath = path.resolve(__dirname, '..', 'templates');\n if (fs.existsSync(devPath)) return devPath;\n const distPath = path.resolve(__dirname, '..', '..', 'templates');\n if (fs.existsSync(distPath)) return distPath;\n throw new Error('Templates directory not found');\n}\n\nfunction walkDir(dir: string): string[] {\n const results: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(full));\n } else {\n results.push(full);\n }\n }\n return results;\n}\n\nfunction processPath(filePath: string, vars: Record<string, string>): string {\n let result = filePath;\n result = result.replace(/__packageName__/g, vars.packageName);\n result = result.replace(/__ProjectName__/g, vars.projectName);\n return result;\n}\n\nasync function promptSimulationType(): Promise<SimulationType> {\n return select<SimulationType>({\n message: 'Select simulation type:',\n choices: [\n { value: 'raw', name: 'Raw ECS', description: 'Manual velocity/position management, no physics engine' },\n { value: 'physics2d', name: 'Physics 2D (Rapier)', description: 'Rapier 2D rigid body physics with auto-managed transforms' },\n { value: 'physics3d', name: 'Physics 3D (Rapier)', description: 'Rapier 3D rigid body physics with top-down 2D rendering' },\n ],\n });\n}\n\nprogram\n .name('create-lagless')\n .description('Scaffold a new Lagless multiplayer game project')\n .version('0.0.38')\n .argument('<project-name>', 'Project name in kebab-case (e.g., my-game)')\n .option('--preset <preset>', 'Project preset', 'pixi-react')\n .option('--port <port>', 'Frontend dev server port', '4203')\n .option('--server-port <port>', 'Backend server port', '3400')\n .option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d')\n .action(async (projectArg: string, options: CreateOptions) => {\n const targetDir = path.resolve(process.cwd(), projectArg);\n const packageName = path.basename(targetDir).toLowerCase();\n const pascalName = toPascalCase(packageName);\n\n if (fs.existsSync(targetDir)) {\n console.error(`Error: Directory \"${targetDir}\" already exists.`);\n process.exit(1);\n }\n\n const templatesDir = getTemplatesDir();\n const presetDir = path.join(templatesDir, options.preset);\n\n if (!fs.existsSync(presetDir)) {\n console.error(`Error: Preset \"${options.preset}\" not found. Available: ${fs.readdirSync(templatesDir).join(', ')}`);\n process.exit(1);\n }\n\n // Determine simulation type — from CLI flag or interactive prompt\n let simulationType: SimulationType;\n if (options.simulationType) {\n const valid: SimulationType[] = ['raw', 'physics2d', 'physics3d'];\n if (!valid.includes(options.simulationType as SimulationType)) {\n console.error(`Error: Invalid simulation type \"${options.simulationType}\". Valid: ${valid.join(', ')}`);\n process.exit(1);\n }\n simulationType = options.simulationType as SimulationType;\n } else {\n simulationType = await promptSimulationType();\n }\n\n // Read package.json from this package to get current lagless version\n const pkgJsonPath = path.resolve(__dirname, '..', 'package.json');\n let laglessVersion = '0.0.38';\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n laglessVersion = pkg.version || laglessVersion;\n } catch {\n // fallback\n }\n\n const vars: Record<string, string> = {\n projectName: pascalName,\n packageName,\n frontendPort: options.port,\n serverPort: options.serverPort,\n laglessVersion,\n simulationType,\n };\n\n console.log(`\\nCreating Lagless project \"${packageName}\"...`);\n console.log(` Preset: ${options.preset}`);\n console.log(` Simulation: ${simulationType}`);\n console.log(` Frontend port: ${options.port}`);\n console.log(` Server port: ${options.serverPort}`);\n console.log(` Target: ${targetDir}\\n`);\n\n const templateFiles = walkDir(presetDir);\n\n for (const templateFile of templateFiles) {\n const relativePath = path.relative(presetDir, templateFile);\n const outputRelative = processPath(relativePath, vars);\n const outputPath = path.join(targetDir, outputRelative);\n const outputDir = path.dirname(outputPath);\n\n const content = fs.readFileSync(templateFile, 'utf-8');\n\n // Only process text files that might contain EJS tags\n const ext = path.extname(templateFile);\n const textExts = ['.ts', '.tsx', '.json', '.yaml', '.yml', '.html', '.css', '.md', '.toml', '.gitignore'];\n\n if (textExts.includes(ext) || ext === '.ejs') {\n const rendered = ejs.render(content, vars, { filename: templateFile });\n const finalPath = ext === '.ejs' ? outputPath.replace(/\\.ejs$/, '') : outputPath;\n\n // Skip writing empty/whitespace-only files (conditional template exclusion)\n if (rendered.trim().length === 0) {\n continue;\n }\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n fs.writeFileSync(finalPath, rendered, 'utf-8');\n } else {\n // Binary or unknown — copy as-is\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n fs.copyFileSync(templateFile, outputPath);\n }\n }\n\n // Remove physics docs that don't match the selected simulationType\n const docsDir = path.join(targetDir, 'docs');\n if (simulationType !== 'physics2d') {\n const f = path.join(docsDir, '08-physics2d.md');\n if (fs.existsSync(f)) fs.rmSync(f);\n }\n if (simulationType !== 'physics3d') {\n const f = path.join(docsDir, '08-physics3d.md');\n if (fs.existsSync(f)) fs.rmSync(f);\n }\n\n // Clone lagless framework source for AI reference\n const sourcesDir = path.join(docsDir, 'sources');\n fs.mkdirSync(sourcesDir, { recursive: true });\n console.log('Cloning lagless framework source for AI reference...');\n try {\n execSync(`git clone --depth 1 https://github.com/GbGr/lagless.git \"${path.join(sourcesDir, 'lagless')}\"`, {\n stdio: 'inherit',\n });\n // Remove .git to save space\n fs.rmSync(path.join(sourcesDir, 'lagless', '.git'), { recursive: true, force: true });\n } catch {\n console.warn('Warning: Could not clone lagless source. AI reference will be unavailable.');\n console.warn('You can manually clone later: git clone --depth 1 https://github.com/GbGr/lagless.git docs/sources/lagless');\n }\n\n // Install dependencies\n console.log('\\nInstalling dependencies...');\n try {\n execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: pnpm install failed. Run it manually after creation.');\n }\n\n // Run ECS codegen to generate code from schema\n console.log('\\nGenerating ECS code from schema...');\n try {\n execSync('pnpm codegen', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: codegen failed. Run \"pnpm codegen\" manually after creation.');\n }\n\n console.log('\\nProject created successfully!\\n');\n console.log('To start developing:');\n console.log(` cd ${packageName}`);\n console.log(' pnpm dev # Start backend + frontend + dev-player\\n');\n console.log('Or run individually:');\n console.log(' pnpm dev:backend # Game server (Bun, watches for changes)');\n console.log(' pnpm dev:frontend # Frontend (Vite HMR)');\n console.log(' pnpm dev:player # Dev-player (multiplayer testing, port 4210)\\n');\n console.log('To regenerate ECS code after schema changes:');\n console.log(' pnpm codegen\\n');\n });\n\nprogram.parse();\n"],"names":["program","fs","path","ejs","execSync","fileURLToPath","select","__filename","url","__dirname","dirname","toPascalCase","kebab","split","map","s","charAt","toUpperCase","slice","join","getTemplatesDir","devPath","resolve","existsSync","distPath","Error","walkDir","dir","results","entry","readdirSync","withFileTypes","full","name","isDirectory","push","processPath","filePath","vars","result","replace","packageName","projectName","promptSimulationType","message","choices","value","description","version","argument","option","action","projectArg","options","targetDir","process","cwd","basename","toLowerCase","pascalName","console","error","exit","templatesDir","presetDir","preset","simulationType","valid","includes","pkgJsonPath","laglessVersion","pkg","JSON","parse","readFileSync","frontendPort","port","serverPort","log","templateFiles","templateFile","relativePath","relative","outputRelative","outputPath","outputDir","content","ext","extname","textExts","rendered","render","filename","finalPath","trim","length","mkdirSync","recursive","writeFileSync","copyFileSync","docsDir","f","rmSync","sourcesDir","stdio","force","warn"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { program } from 'commander';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as ejs from 'ejs';\nimport { execSync } from 'node:child_process';\nimport { fileURLToPath } from 'node:url';\nimport { select } from '@inquirer/prompts';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Read version from package.json once (single source of truth)\nconst _pkgJsonPath = path.resolve(__dirname, '..', 'package.json');\nlet _packageVersion = '0.0.0';\ntry {\n const _pkg = JSON.parse(fs.readFileSync(_pkgJsonPath, 'utf-8'));\n _packageVersion = _pkg.version || _packageVersion;\n} catch {\n // fallback\n}\n\ntype SimulationType = 'raw' | 'physics2d' | 'physics3d';\n\ninterface CreateOptions {\n preset: string;\n port: string;\n serverPort: string;\n simulationType?: SimulationType;\n}\n\nfunction toPascalCase(kebab: string): string {\n return kebab.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');\n}\n\nfunction getTemplatesDir(): string {\n const devPath = path.resolve(__dirname, '..', 'templates');\n if (fs.existsSync(devPath)) return devPath;\n const distPath = path.resolve(__dirname, '..', '..', 'templates');\n if (fs.existsSync(distPath)) return distPath;\n throw new Error('Templates directory not found');\n}\n\nfunction walkDir(dir: string): string[] {\n const results: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...walkDir(full));\n } else {\n results.push(full);\n }\n }\n return results;\n}\n\nfunction processPath(filePath: string, vars: Record<string, string>): string {\n let result = filePath;\n result = result.replace(/__packageName__/g, vars.packageName);\n result = result.replace(/__ProjectName__/g, vars.projectName);\n return result;\n}\n\nasync function promptSimulationType(): Promise<SimulationType> {\n return select<SimulationType>({\n message: 'Select simulation type:',\n choices: [\n { value: 'raw', name: 'Raw ECS', description: 'Manual velocity/position management, no physics engine' },\n { value: 'physics2d', name: 'Physics 2D (Rapier)', description: 'Rapier 2D rigid body physics with auto-managed transforms' },\n { value: 'physics3d', name: 'Physics 3D (Rapier)', description: 'Rapier 3D rigid body physics with top-down 2D rendering' },\n ],\n });\n}\n\nprogram\n .name('create-lagless')\n .description('Scaffold a new Lagless multiplayer game project')\n .version(_packageVersion)\n .argument('<project-name>', 'Project name in kebab-case (e.g., my-game)')\n .option('--preset <preset>', 'Project preset', 'pixi-react')\n .option('--port <port>', 'Frontend dev server port', '4203')\n .option('--server-port <port>', 'Backend server port', '3400')\n .option('--simulation-type <type>', 'Simulation type: raw, physics2d, or physics3d')\n .action(async (projectArg: string, options: CreateOptions) => {\n const targetDir = path.resolve(process.cwd(), projectArg);\n const packageName = path.basename(targetDir).toLowerCase();\n const pascalName = toPascalCase(packageName);\n\n if (fs.existsSync(targetDir)) {\n console.error(`Error: Directory \"${targetDir}\" already exists.`);\n process.exit(1);\n }\n\n const templatesDir = getTemplatesDir();\n const presetDir = path.join(templatesDir, options.preset);\n\n if (!fs.existsSync(presetDir)) {\n console.error(`Error: Preset \"${options.preset}\" not found. Available: ${fs.readdirSync(templatesDir).join(', ')}`);\n process.exit(1);\n }\n\n // Determine simulation type — from CLI flag or interactive prompt\n let simulationType: SimulationType;\n if (options.simulationType) {\n const valid: SimulationType[] = ['raw', 'physics2d', 'physics3d'];\n if (!valid.includes(options.simulationType as SimulationType)) {\n console.error(`Error: Invalid simulation type \"${options.simulationType}\". Valid: ${valid.join(', ')}`);\n process.exit(1);\n }\n simulationType = options.simulationType as SimulationType;\n } else {\n simulationType = await promptSimulationType();\n }\n\n const laglessVersion = _packageVersion;\n\n const vars: Record<string, string> = {\n projectName: pascalName,\n packageName,\n frontendPort: options.port,\n serverPort: options.serverPort,\n laglessVersion,\n simulationType,\n };\n\n console.log(`\\nCreating Lagless project \"${packageName}\"...`);\n console.log(` Preset: ${options.preset}`);\n console.log(` Simulation: ${simulationType}`);\n console.log(` Frontend port: ${options.port}`);\n console.log(` Server port: ${options.serverPort}`);\n console.log(` Target: ${targetDir}\\n`);\n\n const templateFiles = walkDir(presetDir);\n\n for (const templateFile of templateFiles) {\n const relativePath = path.relative(presetDir, templateFile);\n const outputRelative = processPath(relativePath, vars);\n const outputPath = path.join(targetDir, outputRelative);\n const outputDir = path.dirname(outputPath);\n\n const content = fs.readFileSync(templateFile, 'utf-8');\n\n // Only process text files that might contain EJS tags\n const ext = path.extname(templateFile);\n const textExts = ['.ts', '.tsx', '.json', '.yaml', '.yml', '.html', '.css', '.md', '.toml', '.gitignore'];\n\n if (textExts.includes(ext) || ext === '.ejs') {\n const rendered = ejs.render(content, vars, { filename: templateFile });\n const finalPath = ext === '.ejs' ? outputPath.replace(/\\.ejs$/, '') : outputPath;\n\n // Skip writing empty/whitespace-only files (conditional template exclusion)\n if (rendered.trim().length === 0) {\n continue;\n }\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n fs.writeFileSync(finalPath, rendered, 'utf-8');\n } else {\n // Binary or unknown — copy as-is\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n fs.copyFileSync(templateFile, outputPath);\n }\n }\n\n // Remove physics docs that don't match the selected simulationType\n const docsDir = path.join(targetDir, 'docs');\n if (simulationType !== 'physics2d') {\n const f = path.join(docsDir, '08-physics2d.md');\n if (fs.existsSync(f)) fs.rmSync(f);\n }\n if (simulationType !== 'physics3d') {\n const f = path.join(docsDir, '08-physics3d.md');\n if (fs.existsSync(f)) fs.rmSync(f);\n }\n\n // Clone lagless framework source for AI reference\n const sourcesDir = path.join(docsDir, 'sources');\n fs.mkdirSync(sourcesDir, { recursive: true });\n console.log('Cloning lagless framework source for AI reference...');\n try {\n execSync(`git clone --depth 1 https://github.com/GbGr/lagless.git \"${path.join(sourcesDir, 'lagless')}\"`, {\n stdio: 'inherit',\n });\n // Remove .git to save space\n fs.rmSync(path.join(sourcesDir, 'lagless', '.git'), { recursive: true, force: true });\n } catch {\n console.warn('Warning: Could not clone lagless source. AI reference will be unavailable.');\n console.warn('You can manually clone later: git clone --depth 1 https://github.com/GbGr/lagless.git docs/sources/lagless');\n }\n\n // Install dependencies\n console.log('\\nInstalling dependencies...');\n try {\n execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: pnpm install failed. Run it manually after creation.');\n }\n\n // Run ECS codegen to generate code from schema\n console.log('\\nGenerating ECS code from schema...');\n try {\n execSync('pnpm codegen', { cwd: targetDir, stdio: 'inherit' });\n } catch {\n console.error('Warning: codegen failed. Run \"pnpm codegen\" manually after creation.');\n }\n\n console.log('\\nProject created successfully!\\n');\n console.log('To start developing:');\n console.log(` cd ${packageName}`);\n console.log(' pnpm dev # Start backend + frontend + dev-player\\n');\n console.log('Or run individually:');\n console.log(' pnpm dev:backend # Game server (Bun, watches for changes)');\n console.log(' pnpm dev:frontend # Frontend (Vite HMR)');\n console.log(' pnpm dev:player # Dev-player (multiplayer testing, port 4210)\\n');\n console.log('To regenerate ECS code after schema changes:');\n console.log(' pnpm codegen\\n');\n });\n\nprogram.parse();\n"],"names":["program","fs","path","ejs","execSync","fileURLToPath","select","__filename","url","__dirname","dirname","_pkgJsonPath","resolve","_packageVersion","_pkg","JSON","parse","readFileSync","version","toPascalCase","kebab","split","map","s","charAt","toUpperCase","slice","join","getTemplatesDir","devPath","existsSync","distPath","Error","walkDir","dir","results","entry","readdirSync","withFileTypes","full","name","isDirectory","push","processPath","filePath","vars","result","replace","packageName","projectName","promptSimulationType","message","choices","value","description","argument","option","action","projectArg","options","targetDir","process","cwd","basename","toLowerCase","pascalName","console","error","exit","templatesDir","presetDir","preset","simulationType","valid","includes","laglessVersion","frontendPort","port","serverPort","log","templateFiles","templateFile","relativePath","relative","outputRelative","outputPath","outputDir","content","ext","extname","textExts","rendered","render","filename","finalPath","trim","length","mkdirSync","recursive","writeFileSync","copyFileSync","docsDir","f","rmSync","sourcesDir","stdio","force","warn"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
package/package.json
CHANGED
|
@@ -53,8 +53,7 @@ This is a Lagless multiplayer game with three packages:
|
|
|
53
53
|
|
|
54
54
|
### For Physics Changes (<%= simulationType !== 'raw' ? 'APPLIES TO THIS PROJECT' : 'if applicable' %>)
|
|
55
55
|
- [ ] Bodies created with correct BodyType
|
|
56
|
-
- [ ]
|
|
57
|
-
- [ ] ColliderEntityMap rebuilt after state transfer
|
|
56
|
+
- [ ] ColliderEntityMap rebuild is automatic (verify no manual rebuild code)
|
|
58
57
|
- [ ] Collision layers configured correctly
|
|
59
58
|
|
|
60
59
|
## Key Constraints
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"@lagless/pixi-react": "^<%= laglessVersion %>",
|
|
20
20
|
<% if (simulationType === 'physics2d') { -%>
|
|
21
21
|
"@lagless/physics2d": "^<%= laglessVersion %>",
|
|
22
|
-
"@
|
|
22
|
+
"@lagless/physics-shared": "^<%= laglessVersion %>",
|
|
23
|
+
"@dimforge/rapier2d-deterministic-compat": "^0.19.0",
|
|
23
24
|
<% } else if (simulationType === 'physics3d') { -%>
|
|
24
25
|
"@lagless/physics3d": "^<%= laglessVersion %>",
|
|
25
26
|
"@lagless/physics-shared": "^<%= laglessVersion %>",
|
|
26
|
-
"@dimforge/rapier3d-compat": "^0.
|
|
27
|
+
"@dimforge/rapier3d-deterministic-compat": "^0.19.0",
|
|
27
28
|
<% } -%>
|
|
28
29
|
"@abraham/reflection": "^0.12.0",
|
|
29
30
|
"pixi.js": "^8.12.0",
|
|
@@ -9,12 +9,36 @@ export const GridBackground: FC = () => {
|
|
|
9
9
|
const g = graphicsRef.current;
|
|
10
10
|
if (!g) return;
|
|
11
11
|
|
|
12
|
-
const w = <%= projectName %>Arena.width;
|
|
13
12
|
<% if (simulationType === 'physics3d') { -%>
|
|
14
|
-
|
|
13
|
+
// physics3d: world units → screen pixels (top-down view)
|
|
14
|
+
const SCALE = 20;
|
|
15
|
+
const OFFSET_X = 400;
|
|
16
|
+
const OFFSET_Y = 300;
|
|
17
|
+
const arenaW = <%= projectName %>Arena.width;
|
|
18
|
+
const arenaH = <%= projectName %>Arena.depth;
|
|
19
|
+
const screenW = arenaW * SCALE;
|
|
20
|
+
const screenH = arenaH * SCALE;
|
|
21
|
+
const startX = OFFSET_X - screenW / 2;
|
|
22
|
+
const startY = OFFSET_Y - screenH / 2;
|
|
23
|
+
const step = 100;
|
|
24
|
+
|
|
25
|
+
g.clear();
|
|
26
|
+
|
|
27
|
+
for (let x = startX; x <= startX + screenW; x += step) {
|
|
28
|
+
g.moveTo(x, startY);
|
|
29
|
+
g.lineTo(x, startY + screenH);
|
|
30
|
+
}
|
|
31
|
+
for (let y = startY; y <= startY + screenH; y += step) {
|
|
32
|
+
g.moveTo(startX, y);
|
|
33
|
+
g.lineTo(startX + screenW, y);
|
|
34
|
+
}
|
|
35
|
+
g.stroke({ color: 0x333355, width: 1, alpha: 0.4 });
|
|
36
|
+
|
|
37
|
+
g.rect(startX, startY, screenW, screenH);
|
|
38
|
+
g.stroke({ color: 0x6666aa, width: 3, alpha: 0.8 });
|
|
15
39
|
<% } else { -%>
|
|
40
|
+
const w = <%= projectName %>Arena.width;
|
|
16
41
|
const h = <%= projectName %>Arena.height;
|
|
17
|
-
<% } -%>
|
|
18
42
|
const step = 100;
|
|
19
43
|
|
|
20
44
|
g.clear();
|
|
@@ -31,6 +55,7 @@ export const GridBackground: FC = () => {
|
|
|
31
55
|
|
|
32
56
|
g.rect(0, 0, w, h);
|
|
33
57
|
g.stroke({ color: 0x6666aa, width: 3, alpha: 0.8 });
|
|
58
|
+
<% } -%>
|
|
34
59
|
}, []);
|
|
35
60
|
|
|
36
61
|
return <pixiGraphics ref={graphicsRef} />;
|
|
@@ -7,11 +7,6 @@ import {
|
|
|
7
7
|
PlayerJoined,
|
|
8
8
|
ReportHash,
|
|
9
9
|
<%= projectName %>Arena,
|
|
10
|
-
<% if (simulationType !== 'raw') { -%>
|
|
11
|
-
PhysicsRefs,
|
|
12
|
-
PhysicsRefsFilter,
|
|
13
|
-
PlayerFilter,
|
|
14
|
-
<% } -%>
|
|
15
10
|
} from '<%= packageName %>-simulation';
|
|
16
11
|
import { createContext, FC, ReactNode, useContext, useEffect, useState } from 'react';
|
|
17
12
|
import { useTick } from '@pixi/react';
|
|
@@ -80,7 +75,7 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
|
|
|
80
75
|
<% if (simulationType === 'physics2d') { -%>
|
|
81
76
|
// Load Rapier 2D WASM
|
|
82
77
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
-
const RAPIER = (await import('@dimforge/rapier2d-compat')).default as any;
|
|
78
|
+
const RAPIER = (await import('@dimforge/rapier2d-deterministic-compat')).default as any;
|
|
84
79
|
await RAPIER.init();
|
|
85
80
|
const rapier = RAPIER as unknown as RapierModule2d;
|
|
86
81
|
if (disposed) { inputProvider.dispose(); return; }
|
|
@@ -88,7 +83,7 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
|
|
|
88
83
|
<% } else if (simulationType === 'physics3d') { -%>
|
|
89
84
|
// Load Rapier 3D WASM
|
|
90
85
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
-
const RAPIER = (await import('@dimforge/rapier3d-compat')).default as any;
|
|
86
|
+
const RAPIER = (await import('@dimforge/rapier3d-deterministic-compat')).default as any;
|
|
92
87
|
await RAPIER.init();
|
|
93
88
|
const rapier = RAPIER as unknown as RapierModule3d;
|
|
94
89
|
if (disposed) { inputProvider.dispose(); return; }
|
|
@@ -142,20 +137,6 @@ export const RunnerProvider: FC<RunnerProviderProps> = ({ children }) => {
|
|
|
142
137
|
<% } -%>
|
|
143
138
|
}
|
|
144
139
|
|
|
145
|
-
<% if (simulationType !== 'raw') { -%>
|
|
146
|
-
// Hook state transfer to rebuild ColliderEntityMap after receiving external state
|
|
147
|
-
const worldManager = _runner.PhysicsWorldManager;
|
|
148
|
-
_runner.Simulation.addStateTransferHandler(() => {
|
|
149
|
-
worldManager.colliderEntityMap.clear();
|
|
150
|
-
const physicsFilter = _runner.DIContainer.resolve(PhysicsRefsFilter);
|
|
151
|
-
const refs = _runner.DIContainer.resolve(PhysicsRefs);
|
|
152
|
-
const refsUnsafe = refs.unsafe;
|
|
153
|
-
for (const e of physicsFilter) {
|
|
154
|
-
worldManager.registerCollider(refsUnsafe.colliderHandle[e], e);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
<% } -%>
|
|
159
140
|
// Set up keyboard input drainer with hash reporting
|
|
160
141
|
const reportHash = createHashReporter(_runner, {
|
|
161
142
|
reportInterval: <%= projectName %>Arena.hashReportInterval,
|
package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts
CHANGED
|
@@ -74,6 +74,8 @@ import { ECSSystem, IECSSystem, InputProvider, PlayerResources } from '@lagless/
|
|
|
74
74
|
import { MoveInput, PlayerResource, Velocity2d } from '../schema/code-gen/index.js';
|
|
75
75
|
import { <%= projectName %>Arena } from '../arena.js';
|
|
76
76
|
|
|
77
|
+
const finite = (v: number): number => Number.isFinite(v) ? v : 0;
|
|
78
|
+
|
|
77
79
|
@ECSSystem()
|
|
78
80
|
export class ApplyMoveInputSystem implements IECSSystem {
|
|
79
81
|
constructor(
|
|
@@ -88,8 +90,13 @@ export class ApplyMoveInputSystem implements IECSSystem {
|
|
|
88
90
|
for (const rpc of rpcs) {
|
|
89
91
|
const playerResource = this._PlayerResources.get(PlayerResource, rpc.meta.playerSlot);
|
|
90
92
|
const entity = playerResource.safe.entity;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
// Sanitize input
|
|
95
|
+
const dirX = finite(rpc.data.directionX);
|
|
96
|
+
const dirY = finite(rpc.data.directionY);
|
|
97
|
+
|
|
98
|
+
this._Velocity2d.unsafe.velocityX[entity] = dirX * <%= projectName %>Arena.moveSpeed;
|
|
99
|
+
this._Velocity2d.unsafe.velocityY[entity] = dirY * <%= projectName %>Arena.moveSpeed;
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
}
|
|
@@ -35,12 +35,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity directly
|
|
|
35
35
|
|
|
36
36
|
## Creating Bodies and Colliders
|
|
37
37
|
|
|
38
|
-
Use `PhysicsWorldManager2d` to create physics bodies
|
|
38
|
+
Use `PhysicsWorldManager2d` to create physics bodies. The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import { ECSSystem, IECSSystem } from '@lagless/core';
|
|
41
|
+
import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
|
|
42
42
|
import { PhysicsWorldManager2d } from '@lagless/physics2d';
|
|
43
43
|
import { BodyType, CollisionLayers } from '@lagless/physics-shared';
|
|
44
|
+
import { Transform2d, PhysicsRefs } from '../code-gen/core.js';
|
|
44
45
|
|
|
45
46
|
@ECSSystem()
|
|
46
47
|
export class SpawnSystem implements IECSSystem {
|
|
@@ -48,6 +49,7 @@ export class SpawnSystem implements IECSSystem {
|
|
|
48
49
|
private readonly _physics: PhysicsWorldManager2d,
|
|
49
50
|
private readonly _entities: EntitiesManager,
|
|
50
51
|
private readonly _transform: Transform2d,
|
|
52
|
+
private readonly _physicsRefs: PhysicsRefs,
|
|
51
53
|
) {}
|
|
52
54
|
|
|
53
55
|
update(tick: number): void {
|
|
@@ -56,45 +58,60 @@ export class SpawnSystem implements IECSSystem {
|
|
|
56
58
|
this._entities.addComponent(entity, Transform2d);
|
|
57
59
|
this._entities.addComponent(entity, PhysicsRefs);
|
|
58
60
|
|
|
59
|
-
// Set initial position
|
|
60
|
-
this._transform.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
// Set initial position in ECS (including prev for interpolation)
|
|
62
|
+
const t = this._transform.unsafe;
|
|
63
|
+
t.positionX[entity] = 100;
|
|
64
|
+
t.positionY[entity] = 200;
|
|
65
|
+
t.prevPositionX[entity] = 100;
|
|
66
|
+
t.prevPositionY[entity] = 200;
|
|
67
|
+
t.rotation[entity] = 0;
|
|
68
|
+
t.prevRotation[entity] = 0;
|
|
69
|
+
|
|
70
|
+
// Create a dynamic Rapier body and configure it
|
|
71
|
+
const body = this._physics.createDynamicBody();
|
|
72
|
+
body.setTranslation({ x: 100, y: 200 }, true);
|
|
73
|
+
body.setLinearDamping(5.0);
|
|
74
|
+
|
|
75
|
+
// Create a ball collider attached to the body
|
|
76
|
+
const groups = CollisionLayers.get('player');
|
|
77
|
+
const collider = this._physics.createBallCollider(20, body, groups);
|
|
78
|
+
|
|
79
|
+
// Store handles in ECS for later lookup
|
|
80
|
+
const pr = this._physicsRefs.unsafe;
|
|
81
|
+
pr.bodyHandle[entity] = body.handle;
|
|
82
|
+
pr.colliderHandle[entity] = collider.handle;
|
|
83
|
+
pr.bodyType[entity] = BodyType.DYNAMIC;
|
|
84
|
+
pr.collisionLayer[entity] = groups;
|
|
85
|
+
|
|
86
|
+
// Register collider→entity mapping (used by collision events)
|
|
87
|
+
this._physics.registerCollider(collider.handle, entity);
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
```
|
|
83
91
|
|
|
84
92
|
### Collider Shapes
|
|
85
93
|
|
|
94
|
+
All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
|
|
95
|
+
|
|
86
96
|
```typescript
|
|
87
97
|
// Circle
|
|
88
|
-
|
|
98
|
+
this._physics.createBallCollider(radius, parent, groups, activeEvents);
|
|
89
99
|
|
|
90
|
-
// Rectangle
|
|
91
|
-
|
|
100
|
+
// Rectangle (half-extents)
|
|
101
|
+
this._physics.createCuboidCollider(hx, hy, parent, groups, activeEvents);
|
|
92
102
|
|
|
93
103
|
// Capsule
|
|
94
|
-
|
|
104
|
+
this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
105
|
+
|
|
106
|
+
// Convex polygon (returns null if hull computation fails)
|
|
107
|
+
this._physics.createConvexHullCollider(new Float32Array([x1,y1, x2,y2, ...]), parent, groups, activeEvents);
|
|
95
108
|
|
|
96
|
-
//
|
|
97
|
-
|
|
109
|
+
// Triangle mesh (static geometry only)
|
|
110
|
+
this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
|
|
111
|
+
|
|
112
|
+
// Custom collider from a Rapier ColliderDesc
|
|
113
|
+
const desc = this._physics.rapier.ColliderDesc.ball(10).setDensity(2.0).setFriction(0.5);
|
|
114
|
+
this._physics.createColliderFromDesc(desc, parent);
|
|
98
115
|
```
|
|
99
116
|
|
|
100
117
|
## Collision Layers
|
|
@@ -199,9 +216,7 @@ You generally don't interact with this directly — it's managed by `PhysicsWorl
|
|
|
199
216
|
On rollback:
|
|
200
217
|
1. ArrayBuffer is restored → ECS state reverts
|
|
201
218
|
2. Rapier world snapshot is restored → physics state reverts
|
|
202
|
-
3. `
|
|
203
|
-
|
|
204
|
-
**Critical fix applied:** `World.restoreSnapshot()` creates a world with an **empty** QueryPipeline (not serialized). The framework calls `updateSceneQueries()` after restore to fix this. Without it, ray casts and shape casts fail on the first tick after rollback.
|
|
219
|
+
3. `ColliderEntityMap` is rebuilt automatically
|
|
205
220
|
|
|
206
221
|
## State Transfer
|
|
207
222
|
|
|
@@ -217,7 +232,7 @@ This is handled automatically by the physics runner. You don't need to do anythi
|
|
|
217
232
|
```typescript
|
|
218
233
|
import { ECSSystem, IECSSystem, AbstractInputProvider, ECSConfig, EntitiesManager } from '@lagless/core';
|
|
219
234
|
import { MathOps } from '@lagless/math';
|
|
220
|
-
import { PhysicsWorldManager2d
|
|
235
|
+
import { PhysicsWorldManager2d } from '@lagless/physics2d';
|
|
221
236
|
import { BodyType } from '@lagless/physics-shared';
|
|
222
237
|
import { Transform2d, PhysicsRefs, PlayerBody, PlayerFilter, MoveInput } from '../code-gen/core.js';
|
|
223
238
|
|
|
@@ -228,9 +243,9 @@ export class ApplyMoveInputSystem implements IECSSystem {
|
|
|
228
243
|
constructor(
|
|
229
244
|
private readonly _input: AbstractInputProvider,
|
|
230
245
|
private readonly _physics: PhysicsWorldManager2d,
|
|
246
|
+
private readonly _physicsRefs: PhysicsRefs,
|
|
231
247
|
private readonly _playerBody: PlayerBody,
|
|
232
248
|
private readonly _filter: PlayerFilter,
|
|
233
|
-
private readonly _config: ECSConfig,
|
|
234
249
|
) {}
|
|
235
250
|
|
|
236
251
|
update(tick: number): void {
|
|
@@ -244,11 +259,10 @@ export class ApplyMoveInputSystem implements IECSSystem {
|
|
|
244
259
|
if (this._playerBody.unsafe.playerSlot[entity] !== slot) continue;
|
|
245
260
|
|
|
246
261
|
const speed = 300;
|
|
247
|
-
//
|
|
248
|
-
this._physics.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
262
|
+
// Get the Rapier body via its handle stored in PhysicsRefs
|
|
263
|
+
const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
|
|
264
|
+
// Apply velocity directly on the Rapier body
|
|
265
|
+
body.setLinvel({ x: dirX * speed, y: dirY * speed }, true);
|
|
252
266
|
break;
|
|
253
267
|
}
|
|
254
268
|
}
|
|
@@ -37,9 +37,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity
|
|
|
37
37
|
|
|
38
38
|
## Creating Bodies and Colliders
|
|
39
39
|
|
|
40
|
+
The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
|
|
41
|
+
|
|
40
42
|
```typescript
|
|
43
|
+
import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
|
|
41
44
|
import { PhysicsWorldManager3d } from '@lagless/physics3d';
|
|
42
45
|
import { BodyType, CollisionLayers } from '@lagless/physics-shared';
|
|
46
|
+
import { Transform3d, PhysicsRefs } from '../code-gen/core.js';
|
|
43
47
|
|
|
44
48
|
@ECSSystem()
|
|
45
49
|
export class SpawnSystem implements IECSSystem {
|
|
@@ -47,6 +51,7 @@ export class SpawnSystem implements IECSSystem {
|
|
|
47
51
|
private readonly _physics: PhysicsWorldManager3d,
|
|
48
52
|
private readonly _entities: EntitiesManager,
|
|
49
53
|
private readonly _transform: Transform3d,
|
|
54
|
+
private readonly _physicsRefs: PhysicsRefs,
|
|
50
55
|
) {}
|
|
51
56
|
|
|
52
57
|
update(tick: number): void {
|
|
@@ -54,50 +59,74 @@ export class SpawnSystem implements IECSSystem {
|
|
|
54
59
|
this._entities.addComponent(entity, Transform3d);
|
|
55
60
|
this._entities.addComponent(entity, PhysicsRefs);
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
62
|
+
// Set initial position in ECS (including prev for interpolation)
|
|
63
|
+
const t = this._transform.unsafe;
|
|
64
|
+
t.positionX[entity] = 0;
|
|
65
|
+
t.positionY[entity] = 5;
|
|
66
|
+
t.positionZ[entity] = 0;
|
|
67
|
+
t.rotationX[entity] = 0;
|
|
68
|
+
t.rotationY[entity] = 0;
|
|
69
|
+
t.rotationZ[entity] = 0;
|
|
70
|
+
t.rotationW[entity] = 1;
|
|
71
|
+
t.prevPositionX[entity] = 0;
|
|
72
|
+
t.prevPositionY[entity] = 5;
|
|
73
|
+
t.prevPositionZ[entity] = 0;
|
|
74
|
+
t.prevRotationX[entity] = 0;
|
|
75
|
+
t.prevRotationY[entity] = 0;
|
|
76
|
+
t.prevRotationZ[entity] = 0;
|
|
77
|
+
t.prevRotationW[entity] = 1;
|
|
78
|
+
|
|
79
|
+
// Create a dynamic Rapier body and configure it
|
|
80
|
+
const body = this._physics.createDynamicBody();
|
|
81
|
+
body.setTranslation({ x: 0, y: 5, z: 0 }, true);
|
|
82
|
+
body.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
|
|
83
|
+
|
|
84
|
+
// Create a ball collider attached to the body
|
|
85
|
+
const groups = CollisionLayers.get('player');
|
|
86
|
+
const collider = this._physics.createBallCollider(0.5, body, groups);
|
|
87
|
+
|
|
88
|
+
// Store handles in ECS for later lookup
|
|
89
|
+
const pr = this._physicsRefs.unsafe;
|
|
90
|
+
pr.bodyHandle[entity] = body.handle;
|
|
91
|
+
pr.colliderHandle[entity] = collider.handle;
|
|
92
|
+
pr.bodyType[entity] = BodyType.DYNAMIC;
|
|
93
|
+
pr.collisionLayer[entity] = groups;
|
|
94
|
+
|
|
95
|
+
// Register collider→entity mapping (used by collision events)
|
|
96
|
+
this._physics.registerCollider(collider.handle, entity);
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
```
|
|
80
100
|
|
|
81
101
|
### Collider Shapes (3D)
|
|
82
102
|
|
|
103
|
+
All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
|
|
104
|
+
|
|
83
105
|
```typescript
|
|
84
106
|
// Sphere
|
|
85
|
-
|
|
107
|
+
this._physics.createBallCollider(radius, parent, groups, activeEvents);
|
|
86
108
|
|
|
87
|
-
// Box
|
|
88
|
-
|
|
109
|
+
// Box (half-extents, 3 dimensions)
|
|
110
|
+
this._physics.createCuboidCollider(hx, hy, hz, parent, groups, activeEvents);
|
|
89
111
|
|
|
90
112
|
// Capsule
|
|
91
|
-
|
|
113
|
+
this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
114
|
+
|
|
115
|
+
// Cylinder (3D only)
|
|
116
|
+
this._physics.createCylinderCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
92
117
|
|
|
93
|
-
//
|
|
94
|
-
|
|
118
|
+
// Cone (3D only)
|
|
119
|
+
this._physics.createConeCollider(halfHeight, radius, parent, groups, activeEvents);
|
|
95
120
|
|
|
96
|
-
// Convex hull
|
|
97
|
-
|
|
121
|
+
// Convex hull (returns null if hull computation fails)
|
|
122
|
+
this._physics.createConvexHullCollider(new Float32Array([...]), parent, groups, activeEvents);
|
|
98
123
|
|
|
99
|
-
// Triangle mesh (static only)
|
|
100
|
-
|
|
124
|
+
// Triangle mesh (static geometry only)
|
|
125
|
+
this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
|
|
126
|
+
|
|
127
|
+
// Custom collider from a Rapier ColliderDesc
|
|
128
|
+
const desc = this._physics.rapier.ColliderDesc.ball(0.5).setDensity(2.0).setFriction(0.5);
|
|
129
|
+
this._physics.createColliderFromDesc(desc, parent);
|
|
101
130
|
```
|
|
102
131
|
|
|
103
132
|
## Collision Layers and Events
|
|
@@ -281,11 +310,9 @@ export const systems = [
|
|
|
281
310
|
On rollback:
|
|
282
311
|
1. ArrayBuffer is restored → ECS state reverts
|
|
283
312
|
2. Rapier 3D world snapshot is restored → physics state reverts
|
|
284
|
-
3. `
|
|
313
|
+
3. `ColliderEntityMap` is rebuilt automatically
|
|
285
314
|
4. KCC controllers are recreated via `recreateAll()`
|
|
286
315
|
|
|
287
|
-
**Critical fix:** `World.restoreSnapshot()` creates a world with an empty QueryPipeline. The framework calls `updateSceneQueries()` after restore. Without this, `computeColliderMovement()` queries fail on the first tick after rollback.
|
|
288
|
-
|
|
289
316
|
## State Transfer
|
|
290
317
|
|
|
291
318
|
After `applyExternalState()`:
|
|
@@ -178,17 +178,12 @@ this._transform.unsafe.positionX[entity] = newX; // Physics will overwrite!
|
|
|
178
178
|
```
|
|
179
179
|
**Correct:**
|
|
180
180
|
```typescript
|
|
181
|
-
this._physics.
|
|
181
|
+
const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
|
|
182
|
+
body.setLinvel({ x: vx, y: vy }, true);
|
|
182
183
|
// or
|
|
183
|
-
|
|
184
|
+
body.applyImpulse({ x: fx, y: fy }, true);
|
|
184
185
|
```
|
|
185
|
-
**Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity.
|
|
186
|
-
|
|
187
|
-
### Forgetting updateSceneQueries After Restore
|
|
188
|
-
|
|
189
|
-
The framework handles this automatically, but if you're doing custom Rapier operations:
|
|
190
|
-
**Always** call `world.updateSceneQueries()` after `World.restoreSnapshot()`.
|
|
191
|
-
**Why:** Restored worlds have empty QueryPipeline — ray casts and shape casts will miss all colliders.
|
|
186
|
+
**Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity on the Rapier body.
|
|
192
187
|
|
|
193
188
|
## Multiplayer
|
|
194
189
|
|