- name
- camera
- description
- Camera in FlatRedBall2. Use when working with camera setup, background color, world bounds, window resolution, scrolling, screen shake, coordinate conversion between world and screen space, AspectPolicy / ResizeMode / DominantAxis, or Camera.OrthogonalWidth/OrthogonalHeight. Trigger on any camera-related question including viewport, following a player, letterboxing, or screen boundaries.
# Camera in FlatRedBall2
Every `Screen` has a `Camera` property. Access it directly — no initialization required.
```csharp
Screen.Camera // type: FlatRedBall2.Rendering.Camera
```
## Background Color
```csharp
Camera.BackgroundColor = Color.Black; // default
Camera.BackgroundColor = Color.CornflowerBlue;
```
Set this in `CustomInitialize`. It applies immediately.
## World Bounds
`Camera.OrthogonalWidth` and `Camera.OrthogonalHeight` are the visible world extents at `Zoom = 1`. They are computed by the engine from `DisplaySettings` — do not assign directly.
World coordinates are **centered at the origin**:
- X ∈ [−OrthogonalWidth/2, OrthogonalWidth/2]
- Y ∈ [−OrthogonalHeight/2, OrthogonalHeight/2]
Y+ is **up** (see `physics-and-movement`).
```csharp
wall.Y = -Camera.OrthogonalHeight / 2f; // bottom of screen
```
Prefer `Camera.Left`/`Right`/`Top`/`Bottom` for edges — they account for `Zoom` and camera position.
> **Screen-edge boundaries** (keeping entities in bounds) are a collision concern, not a camera concern — use wall entities or `TileShapes`. See the `collision-relationships` and `shapes` skills.
## DisplaySettings — Resolution, Aspect, Zoom
`FlatRedBallService.Default.DisplaySettings` controls how the camera is configured at each screen start. Set these before calling `Start<T>()` or between screens.
```csharp
var ds = FlatRedBallService.Default.DisplaySettings;
ds.ResolutionWidth = 1280; // design world width in world units
ds.ResolutionHeight = 720; // design world height in world units
ds.AspectPolicy = AspectPolicy.Locked; // default: pillar/letterbox to aspect
ds.FixedAspectRatio = null; // default: derive from Resolution; set to override
ds.DominantAxis = DominantAxis.Height; // default: pin design height when aspect differs
ds.ResizeMode = ResizeMode.StretchVisibleArea; // default: same world always visible
ds.LetterboxColor = Color.Black;
ds.WindowMode = WindowMode.Windowed; // or FullscreenBorderless
ds.PreferredWindowWidth = 1280; // startup window pixel size (null = leave as-is)
ds.PreferredWindowHeight = 720;
ds.AllowUserResizing = false; // default: fixed canvas; set true to let the player resize
ds.TextureFilterMode = TextureFilterMode.Point; // default: crisp pixel art; Linear for smooth-scaled/card art
```
## Three Knobs: AspectPolicy × ResizeMode × DominantAxis
Three orthogonal settings decide what the player sees on resize. The defaults are the safe choice — pillarbox to design aspect, world stays put, no distortion ever.
### AspectPolicy
- **`Locked`** (default) — viewport is centered inside the window with letterbox/pillarbox bars to enforce the effective aspect ratio. Aspect comes from `FixedAspectRatio` if set, else `ResolutionWidth/ResolutionHeight`.
- **`Free`** — viewport fills the window. The visible world's aspect follows the window's; resizing changes how much world is visible.
### ResizeMode
- **`StretchVisibleArea`** (default) — the dominant-axis world extent is fixed at its `Resolution*` value. A larger window just rescales pixels. Combined with `Locked` aspect, the entire design world is always exactly visible.
- **`IncreaseVisibleArea`** — pixels-per-world-unit is fixed at `Zoom`. A larger window reveals more world (proportionally on both axes under Locked, on the non-dominant axis under Free + Stretch — see below).
### DominantAxis
Consulted under `Free`, or under `Locked` if `FixedAspectRatio` is set to a value that differs from the design ratio.
- **`Height`** (default) — design height stays at `ResolutionHeight`; design width tracks the effective aspect.
- **`Width`** — design width stays at `ResolutionWidth`; design height tracks.
## Recipes
**Default — locked aspect, no surprises (recommended for fixed-camera games):**
```csharp
ds.ResolutionWidth = 240; ds.ResolutionHeight = 320;
ds.PreferredWindowWidth = 720; ds.PreferredWindowHeight = 960; // 3× scale
// AspectPolicy.Locked, ResizeMode.StretchVisibleArea, FixedAspectRatio=null are all defaults
ds.AllowUserResizing = true;
```
Result: 240×320 world always visible. Resize freely — the rendered area pillarboxes/letterboxes to keep 0.75 aspect; the playfield never grows.
**Pixel-art crisp scaling that grows with the window:**
```csharp
ds.AspectPolicy = AspectPolicy.Free;
ds.ResizeMode = ResizeMode.IncreaseVisibleArea;
// Pixels-per-unit comes from PreferredWindowWidth/Height vs ResolutionWidth/Height,
// or from a CustomInitialize override of Camera.Zoom for non-default starts.
```
No bars; bigger window reveals more world on both axes. Sprites stay at native pixel size.
**Side-scroller — fixed camera height, world width tracks window aspect:**
```csharp
ds.AspectPolicy = AspectPolicy.Free;
ds.DominantAxis = DominantAxis.Height;
ds.ResizeMode = ResizeMode.StretchVisibleArea;
```
Window grows wider → more level visible horizontally. Window grows taller → height stays at `ResolutionHeight`, pixels just bigger.
**Explicit display ratio different from design (e.g. 320×240 design always rendered 16:9):**
```csharp
ds.ResolutionWidth = 320; ds.ResolutionHeight = 240;
ds.AspectPolicy = AspectPolicy.Locked;
ds.FixedAspectRatio = 16f / 9f; // override; design gets letterboxed inside 16:9
ds.DominantAxis = DominantAxis.Height;
```
## Runtime Camera Zoom
```csharp
Camera.Zoom = 2f; // zoom in: shows half the world area
Camera.Zoom = 0.5f; // zoom out: shows double the world area
```
`Camera.Zoom` is reset to `1f` at the start of each new screen. Screens that want a non-default starting zoom assign `Camera.Zoom` in `CustomInitialize`. Most games leave runtime zoom at 1 — the on-screen scale comes from the window-vs-resolution ratio, not from `Zoom`.
## Window Resolution and Fullscreen
`Camera.OrthogonalWidth/Height` do **not** control the actual window pixel size. Window size is set via `DisplaySettings` and applied in two ways:
**Startup** — `Initialize<T>` sizes the window from the starting screen's `PreferredDisplaySettings`, falling back to the engine's, so set them in the constructor and the window opens at that size:
```csharp
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
var ds = FlatRedBallService.Default.DisplaySettings;
ds.PreferredWindowWidth = 1280;
ds.PreferredWindowHeight = 720;
}
```
A Glue project's own display block wins over both — it is applied as the project loads, before sizing.
**Runtime** (settings menu, F11 toggle) — call `ApplyWindowSettings` at any time:
```csharp
var newMode = Engine.DisplaySettings.WindowMode == WindowMode.Windowed
? WindowMode.FullscreenBorderless
: WindowMode.Windowed;
Engine.ApplyWindowSettings(new DisplaySettings { WindowMode = newMode });
```
## Camera Position (Scrolling)
For fixed-screen games (Pong, etc.), leave `Camera.X = 0` and `Camera.Y = 0` (the defaults).
For manual scrolling, set `Camera.X`/`Camera.Y` each frame from `CustomActivity`:
```csharp
Camera.X = player.X;
Camera.Y = player.Y;
```
## Camera Physics — Smooth Movement and Transitions
`Camera` has the same velocity and acceleration properties as `Entity`:
```csharp
Camera.VelocityX // world units/sec — applied each frame by the engine physics loop
Camera.VelocityY
Camera.AccelerationX
Camera.AccelerationY
```
The camera is updated by the same physics loop as entities — set velocity or acceleration and it moves automatically each frame. **Do not lerp `Camera.X`/`Camera.Y` manually** — use velocity instead.
```csharp
// Slide camera right one screen width per second
Camera.VelocityX = Camera.OrthogonalWidth;
```
For a timed one-shot slide, use an async delay to stop it:
```csharp
float targetX = Camera.X + Camera.OrthogonalWidth;
Camera.VelocityX = Camera.OrthogonalWidth;
await Engine.Time.DelaySeconds(1.0, Token);
Camera.VelocityX = 0f;
Camera.X = targetX; // snap to exact position to eliminate drift
```
> **Note:** Camera has no `Drag` property — velocity must be zeroed explicitly.
## CameraControllingEntity — Automatic Following
`CameraControllingEntity` (in `FlatRedBall2.Entities`) is an `Entity` subclass that handles following, map clamping, deadzone, pixel-snapping, and screen shake automatically.
**Always create it via `Factory<CameraControllingEntity>`** — Factory calls `CustomInitialize`, which wires up the camera. `Screen.Register` does NOT call `CustomInitialize`.
```csharp
private Factory<CameraControllingEntity> _cameraFactory = null!;
public override void CustomInitialize()
{
_cameraFactory = new Factory<CameraControllingEntity>(this);
var mapBounds = new BoundsRectangle(2560f, 1440f); // centered at origin
var cam = _cameraFactory.Create();
cam.Target = player;
cam.Map = mapBounds; // clamps camera; null = no bounds
cam.TargetApproachStyle = TargetApproachStyle.Smooth;
cam.TargetApproachCoefficient = 8f;
}
```
**When using a `TileMap`, derive bounds from the map instead of hardcoding:**
```csharp
cam.Map = new BoundsRectangle(map.X + map.Width / 2f, map.Y - map.Height / 2f, map.Width, map.Height);
```
**Approach styles:** `Immediate`, `Smooth`, `ConstantSpeed`.
**Deadzone:**
```csharp
cam.ScrollingWindowWidth = 200f;
cam.ScrollingWindowHeight = 120f;
```
**Pixel-perfect snapping** — on by default (`SnapToPixel = true`). Uses `Camera.PixelsPerUnit`.
**Screen shake** (async; pass `Token` to cancel on screen transition):
```csharp
_ = cam.ShakeScreen(radius: 8f, durationInSeconds: 0.4f, Token);
```
**Multi-target auto-zoom** (frames all targets in view):
```csharp
cam.EnableAutoZooming(defaultZoom: Camera.Zoom, furthestMultiplier: 3f);
```
## Coordinate Conversion
```csharp
System.Numerics.Vector2 screenPos = Camera.WorldToScreen(worldPos);
System.Numerics.Vector2 worldPos = Camera.ScreenToWorld(screenPos);
```
## Free-Roaming Camera (No Entity to Follow)
Drive the camera directly from screen `CustomActivity`. Set velocity each frame (no `Drag` — zero it explicitly):
```csharp
const float PanSpeed = 400f;
const float MapHalfW = 128 * 16 / 2f;
const float MapHalfH = 128 * 16 / 2f;
var kb = Engine.Input.Keyboard;
Camera.VelocityX = 0f;
Camera.VelocityY = 0f;
if (kb.IsKeyDown(Keys.A) || kb.IsKeyDown(Keys.Left)) Camera.VelocityX = -PanSpeed;
if (kb.IsKeyDown(Keys.D) || kb.IsKeyDown(Keys.Right)) Camera.VelocityX = PanSpeed;
if (kb.IsKeyDown(Keys.S) || kb.IsKeyDown(Keys.Down)) Camera.VelocityY = -PanSpeed;
if (kb.IsKeyDown(Keys.W) || kb.IsKeyDown(Keys.Up)) Camera.VelocityY = PanSpeed;
Camera.X = Math.Clamp(Camera.X, -MapHalfW + Camera.OrthogonalWidth / 2f,
MapHalfW - Camera.OrthogonalWidth / 2f);
Camera.Y = Math.Clamp(Camera.Y, -MapHalfH + Camera.OrthogonalHeight / 2f,
MapHalfH - Camera.OrthogonalHeight / 2f);
```
### Scroll Wheel Zoom with Clamp
```csharp
const float ZoomStep = 0.1f, ZoomMin = 0.5f, ZoomMax = 3.0f;
int scroll = Engine.Input.Cursor.ScrollWheelDelta;
if (scroll != 0)
{
float direction = scroll > 0 ? 1f : -1f;
Camera.Zoom = Math.Clamp(Camera.Zoom + direction * ZoomStep, ZoomMin, ZoomMax);
}
```
`Camera.Zoom` is reset on every screen transition. For a persistent zoom, store it in a field and re-apply in `CustomInitialize`.
## Gotchas
- **`Camera.OrthogonalWidth/Height` ≠ window pixel size.** Under default `Locked` + `Stretch`, OrthogonalWidth/Height equal `ResolutionWidth/Height` regardless of window size — the window is just rescaled to fit. Under `Free` + `Stretch`, the non-dominant axis tracks the window aspect; under `IncreaseVisibleArea`, both axes track viewport pixels.
- **Express on-screen scale via window-vs-resolution, not zoom.** A 426×240 design rendered to a 1280×720 window auto-scales 3×. Setting `Camera.Zoom = 3` on top of that zooms in 3× (showing 1/3 of the design world) — almost always wrong. Reserve `Camera.Zoom` for runtime cinematic effects.
- **Do not set `OrthogonalWidth`/`OrthogonalHeight` directly.** They have `internal set` and are managed by the engine from `DisplaySettings`. Use `Camera.Zoom` for runtime zoom.
- **Viewport edge coordinates**: Use `Camera.Left`, `Camera.Right`, `Camera.Top`, `Camera.Bottom` — these are Zoom- and position-correct. Do not compute edges from `Camera.X ± Camera.OrthogonalWidth / 2f`.
- **Gum HUD (`Camera.Add`/`Screen.Add`) is independent of Camera position, not `Zoom`.** Gum X/Y are screen pixels, Y-down from the top-left — panning (`Camera.X`/`Y`) never shifts them, but `Zoom` rescales them by design (HUD shares `PixelsPerUnit` with world content, for camera-relative HUD effects). For HUD that must ignore `Zoom` — e.g. a cinematic zoom-in that shouldn't touch the leaderboard — use a `Layer { IsScreenSpace = true }` (per-camera, viewport-scoped, correct in split-screen) or `Screen.AddOverlay` (screen-wide). See `gum-integration`.
- **`TextureFilterMode` (Point/Linear) is per-screen via `PreferredDisplaySettings`**, same override mechanism as everything else above — defaults to `Point`; the four sprite render batches read it from `FlatRedBallService.Default.DisplaySettings`.
- **`AllowUserResizing` defaults to `false`.** Set to `true` opt-in. The default `Locked` aspect policy means resize is safe (pillarboxes), but you must opt in for the player to be able to drag window borders.
GitHubで見る