This document contains rules, conventions, and best practices for AI agents and developers working on the PlayCanvas Engine codebase.
PlayCanvas is an open-source WebGL/WebGPU game engine written in JavaScript. It's a performance-critical library used by thousands of developers worldwide.
- Language: JavaScript (ES2022) with JSDoc for TypeScript type definitions
- Module System: ES Modules
- Node Version: >=18.0.0
- Build System: Rollup
- Testing: Mocha + Chai + Sinon
- Linting: ESLint with @playcanvas/eslint-config
- License: MIT
- Follow ESLint rules: Always run
npm run lintbefore committing- Important: Only fix lint issues in code you are actively modifying or creating
- Do not fix pre-existing lint issues in unrelated code unless specifically asked
- Focus on ensuring new and refactored code is lint-free
- Use JSDoc comments: All public APIs must have comprehensive JSDoc documentation
- Module imports: Use ES6 import/export syntax
- Naming conventions:
- Classes: PascalCase (e.g.,
GraphicsDevice,Entity) - Functions/methods: camelCase (e.g.,
createShader,setPosition) - Constants: UPPER_SNAKE_CASE (e.g.,
PIXELFORMAT_RGBA8)
- Classes: PascalCase (e.g.,
- Source files: All engine source code goes in
src/ - Directory structure:
src/core/- Core utilities and data structuressrc/platform/- Platform-specific code (graphics, audio, input)src/scene/- Scene graph, rendering, materials, shaderssrc/framework/- High-level components and application frameworksrc/extras/- Optional extras and utilities
- Build output: Generated files go in
build/(never edit these directly) - Examples: Live in
examples/src/examples/ - Tests: Unit tests go in
test/with.mjsextension - File naming: Module file names should match the main class they contain
- Use kebab-case for file names (e.g.,
graphics-device.jsforGraphicsDeviceclass) - If a class is renamed, the file should be renamed to match
- Multiple related classes can share a file if they're tightly coupled
- Use kebab-case for file names (e.g.,
The codebase follows a strict hierarchical structure to maintain clean architecture:
core → platform → scene → framework
Rules:
- Lower-level modules cannot import from higher-level modules
- Lower-level modules cannot use instances from higher-level modules
- Example:
core/cannot import fromplatform/,scene/, orframework/ - Example:
scene/cannot import fromframework/
Known Exception:
CameraComponent(fromframework/) is currently used in multiple places at thescene/level- Do not introduce new exceptions unless explicitly requested and confirmed
- When in doubt, ask before breaking the hierarchy
This hierarchy ensures:
- Clean separation of concerns
- Prevents circular dependencies
- Makes the codebase more maintainable and testable
- JSDoc is mandatory for all public APIs:
/** * Brief description of the function. * * @param {string} name - Parameter description. * @param {number} [optional=0] - Optional parameter with default. * @returns {boolean} Return value description. * @example * const result = myFunction('test', 5); */
- Include examples for complex APIs
- Document side effects: Mention if a function modifies state
- Link related APIs: Use
@seetags to cross-reference - Mark deprecations: Use
@deprecatedwith migration instructions
- JSDoc comments are used to generate TypeScript definitions
- Run
npm run build:typesto generate.d.tsfiles - Test types with
npm run test:types - Use proper JSDoc type annotations:
@type {TypeName}for variables@param {TypeName} paramNamefor parameters@returns {TypeName}for return values- Support for generics, unions, and complex types
- Type-only imports: Use
@importfor types referenced in JSDoc comments- These imports are only for type information, not runtime code
- Place at the top of the file in a JSDoc comment block
- Example:
/** * @import { Texture } from './texture.js' * @import { Shader } from './shader.js' */
- These help TypeScript understand types without adding runtime dependencies
- Write tests for all new features and bug fixes if instructed
- Test location:
test/directory, organized by module - Test naming: Use descriptive names that explain what is being tested
- Run tests:
npm test(ornpm run test:coveragefor coverage) - Test structure:
describe('ClassName', function () { describe('#methodName', function () { it('should do something specific', function () { // Test implementation }); }); });
This is a performance-critical engine. Always consider:
- Avoid allocations in hot paths: Reuse objects, use object pools
- Minimize function calls: Inline critical code when necessary
- Cache property access: Store frequently accessed properties in local variables
- Use typed arrays: For numeric data (Float32Array, Uint8Array, etc.)
- Multi-backend support: Code must work with both WebGL2 and WebGPU
- Use abstraction layers: Don't call WebGL/WebGPU APIs directly in high-level code
- Shader code: Maintain both GLSL and WGSL versions
- GLSL:
src/scene/shader-lib/glsl/ - WGSL:
src/scene/shader-lib/wgsl/
- GLSL:
- NullGraphicsDevice: A dummy graphics device for headless/testing scenarios
- When adding public API methods to
GraphicsDevice, add stub implementations toNullGraphicsDevice - Stub methods should be empty or return safe default values to avoid crashes
- This ensures the engine can run without a real graphics backend for testing/server-side use
- When adding public API methods to
- Backward compatibility matters: Breaking changes require major version bump
- Deprecation process:
- Mark API as
@deprecatedwith alternatives - Add console warning in development builds
- Keep deprecated code for at least one major version
- Consider removing jsdocs completely
- Mark API as
- Never remove public APIs without proper deprecation cycle
- Source is in
src/: Never edit files inbuild/directory - Module exports: Main exports defined in
src/index.js
- Minimal dependencies: Avoid adding new dependencies unless absolutely necessary
- Types only:
@types/*and@webgpu/typesare the main dependencies
- Debug class: Use
Debugclass (src/core/debug.js) for logging and assertions- Methods include:
assert(),warn(),warnOnce(),error(),deprecated(),log(),trace() - Important: All Debug methods are stripped out in production builds
- Use
*Once()variants to avoid spam in loops or frequent calls - Don't use Debug in hot paths - even in debug builds, excessive logging impacts performance
- Methods include:
- DebugHelper class: Helper methods for debugging (also stripped in production)
setName(),setLabel(),setDestroyed()for marking objects
- Explain "why" not "what": Code should be self-documenting, but comments help with quick understanding
- Complex algorithms: Explain the approach and any non-obvious optimizations
- TODOs: Include issue reference or context
// TODO: Optimize this when texture streaming is implemented (#1234) - Avoid very obvious comments: Don't state what the code clearly does
- Clear commit messages: Use conventional commits format
feat: Add feature descriptionfix: Bug fix descriptionperf: Performance improvement descriptiondocs: Documentation updaterefactor: Code refactoringtest: Test updates
- Reference issues: Include issue number in commit message in format 'Fixed #1234'
- Small, focused commits: Each commit should be a logical unit
- No generated files: Don't commit files in
build/directory
- Modern browsers only: ES6+ features are allowed
- No polyfills in engine: Users can add their own if needed (except
src/polyfill/) - WebGL 2.0 minimum: WebGL 1.0 is not supported
- WebGPU support: Must maintain compatibility with WebGPU API
// Prefer class syntax with TypeScript-like property declarations
class MyClass {
/**
* @type {GraphicsDevice}
*/
device;
/**
* @type {string}
*/
name;
constructor(device, options = {}) {
this.device = device;
this.name = options.name ?? 'default';
}
destroy() {
// Clean up resources
this.device = null;
}
}// Always provide destroy() method for objects holding resources
class Resource {
constructor() {
this._resource = createResource();
}
destroy() {
this._resource?.destroy();
this._resource = null;
}
}Always address the root cause of issues rather than implementing workarounds that hide or suppress problems:
- Identify the root cause: When you encounter an error or unexpected behavior, investigate why it's happening
- Don't mask symptoms: Avoid solutions that simply hide errors or suppress warnings without fixing the underlying issue
- Fix at the source: When you identify the root cause, fix it where the problem originates, not where it manifests
- Don't use
var: Useconstorlet(except in legacyscripts/directory) - Avoid
anytypes: Be specific in JSDoc type annotations - No global state: Everything should be instance-based
- Exception: Module-scope variables for local optimization are allowed (e.g., reusable Mat4 instances)
- These must never be exported and should only be used within the module
- Don't bypass abstractions: Use the platform API, not direct WebGL/WebGPU calls
- Don't suppress linter warnings: Fix the underlying issue
- No allocations in render loop: Pre-allocate and reuse if feasible
- Don't use
try/catchin hot paths: It prevents optimizations - No string concatenation in loops: Build arrays and join
- Don't create functions in loops: Define functions outside
- Read existing code first: Understand the context and patterns
- Follow existing style: Match the style of surrounding code
- Lint your changes: Run
npm run lint - Update documentation: Modify JSDoc comments when changing APIs
- Consider performance: This is a real-time engine, every microsecond counts
- Check both WebGL and WebGPU: Changes may affect both backends
- Examples go in
examples/src/examples/ - Follow existing example structure (see other
.example.mjsfiles) - Include descriptive comments
- Keep examples simple and focused on one feature
- Format as a single code block: Always deliver PR descriptions wrapped in triple backticks for easy copy/paste
- Structure:
- Brief title and overview
- Bullet points for functionality changes
- Technical details section (if relevant)
- Clearly list all public API changes with before/after code examples
- List updated examples (if applicable)
- Performance considerations (if relevant)
- Focus on user-facing changes: What developers using the engine will see/use
- Be concise but complete: Include all breaking changes and new APIs
- Avoid excessive detail: Group related changes together, don't list every tiny implementation detail or internal refactoring
- Only document public APIs: Do not list functionality tagged with
@ignore,@protected, or@privateas these are internal implementation details
- API Reference: https://api.playcanvas.com/engine/
- User Manual: https://developer.playcanvas.com/user-manual/engine/
- Developer Site: https://github.com/playcanvas/developer-site
- For large features, ask to add documentation to the User Manual
- Manual pages are Markdown files in the
docs/directory
- Examples: https://playcanvas.github.io
- Forum: https://forum.playcanvas.com
- Discord: https://discord.gg/RSaMRzg
- GitHub Issues: https://github.com/playcanvas/engine/issues
When in doubt:
- Look at similar existing code in the codebase
- Check the ESLint configuration
- Review recent commits for patterns
- If unclear or multiple valid approaches exist, ask instead of picking a possibly incorrect solution
Remember: This is a library used by thousands of developers. Quality, performance, and stability are paramount. When in doubt, prefer conservative, well-tested changes over clever optimizations.