Skip to content

Rendering

ESEngine uses a component-based rendering system. Add rendering components to entities and the engine handles drawing automatically.

Sprites

The Sprite component makes an entity visible:

import { Commands, Sprite, LocalTransform } from 'esengine';
defineSystem([Commands()], (cmds) => {
cmds.spawn()
.insert(Sprite, {
texture: 0, // Texture ID
color: { x: 1, y: 1, z: 1, w: 1 }, // RGBA (white)
size: { x: 64, y: 64 }, // Width, height
uvOffset: { x: 0, y: 0 }, // Texture offset
uvScale: { x: 1, y: 1 }, // Texture scale
layer: 0, // Render order
flipX: false,
flipY: false
})
.insert(LocalTransform, {
position: { x: 100, y: 100, z: 0 }
});
});

Sprite Properties

PropertyTypeDescription
texturenumberTexture resource ID
colorVec4Tint color (RGBA, 0-1)
sizeVec2Sprite dimensions in pixels
uvOffsetVec2Texture coordinate offset
uvScaleVec2Texture coordinate scale
layernumberRender order (higher = on top)
flipXbooleanFlip horizontally
flipYbooleanFlip vertically

Colored Rectangle

For simple colored rectangles without textures:

cmds.spawn()
.insert(Sprite, {
texture: 0, // No texture
color: { x: 1, y: 0, z: 0, w: 1 }, // Red
size: { x: 50, y: 50 }
})
.insert(LocalTransform, {
position: { x: 200, y: 200, z: 0 }
});

Camera

A Camera component is required to see anything. Create one in your startup system:

cmds.spawn()
.insert(Camera, {
projectionType: 1, // 0 = Perspective, 1 = Orthographic
fov: 60, // Field of view (perspective only)
orthoSize: 400, // Half-height in world units (ortho only)
nearPlane: 0.1,
farPlane: 1000,
aspectRatio: 1, // Usually set automatically
isActive: true, // Only one camera should be active
priority: 0 // Higher priority camera wins
})
.insert(LocalTransform, {
position: { x: 0, y: 0, z: 10 } // Camera position
});

Orthographic Camera

Best for 2D games. The orthoSize determines how much of the world is visible:

// Camera showing 800x600 world units
cmds.spawn()
.insert(Camera, {
projectionType: 1,
orthoSize: 300, // Half of 600
isActive: true
})
.insert(LocalTransform, {
position: { x: 400, y: 300, z: 10 } // Center of 800x600 world
});

Moving the Camera

Query and modify the camera’s transform:

defineSystem(
[Res(Input), Query(LocalTransform, Camera)],
(input, query) => {
for (const [entity, transform, camera] of query) {
// Pan camera with arrow keys
if (input.isKeyDown('ArrowLeft')) {
transform.position.x -= 5;
}
if (input.isKeyDown('ArrowRight')) {
transform.position.x += 5;
}
}
}
);

Transform

Every visible entity needs a LocalTransform:

cmds.spawn()
.insert(LocalTransform, {
position: { x: 100, y: 200, z: 0 },
rotation: { w: 1, x: 0, y: 0, z: 0 }, // Quaternion
scale: { x: 1, y: 1, z: 1 }
})
.insert(Sprite, { ... });

Rotation

Rotation uses quaternions. For 2D rotation around Z-axis:

// Rotate sprite over time
defineSystem(
[Res(Time), Query(LocalTransform, Sprite)],
(time, query) => {
for (const [entity, transform, sprite] of query) {
const angle = time.elapsed; // Radians
const halfAngle = angle / 2;
transform.rotation = {
w: Math.cos(halfAngle),
x: 0,
y: 0,
z: Math.sin(halfAngle)
};
}
}
);

Scale

// Double size
transform.scale = { x: 2, y: 2, z: 1 };
// Flip horizontally
transform.scale = { x: -1, y: 1, z: 1 };

Render Order

Sprites are drawn in order by:

  1. Z position - Lower Z draws first (behind)
  2. Layer - Higher layer draws on top
// Background (behind everything)
cmds.spawn()
.insert(Sprite, { layer: 0, ... })
.insert(LocalTransform, { position: { x: 0, y: 0, z: -10 } });
// Player (middle)
cmds.spawn()
.insert(Sprite, { layer: 1, ... })
.insert(LocalTransform, { position: { x: 0, y: 0, z: 0 } });
// UI (on top)
cmds.spawn()
.insert(Sprite, { layer: 10, ... })
.insert(LocalTransform, { position: { x: 0, y: 0, z: 10 } });

Example: Animated Sprite

import {
defineSystem, defineComponent, Schedule,
Res, Time, Query, LocalTransform, Sprite
} from 'esengine';
const Animation = defineComponent('Animation', {
frames: 4,
currentFrame: 0,
frameTime: 0.1,
elapsed: 0
});
// Animation system
app.addSystemToSchedule(Schedule.Update, defineSystem(
[Res(Time), Query(Sprite, Animation)],
(time, query) => {
for (const [entity, sprite, anim] of query) {
anim.elapsed += time.delta;
if (anim.elapsed >= anim.frameTime) {
anim.elapsed = 0;
anim.currentFrame = (anim.currentFrame + 1) % anim.frames;
// Update UV offset for sprite sheet
sprite.uvOffset.x = anim.currentFrame / anim.frames;
}
}
}
));