Files
sanspie b6dd46f523
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 14m14s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m1s
ci / release (push) Failing after 14m38s
Stage 10a follow-up: camera scene tests, rewind check, value Adjust
- camera.NewService and SetAccessibility; AttachWorld wires the accessibility settings
- tests/camera: priorities, blends, cuts, zones, follow, bounds, outputs, shake, zero allocations, BrainStep benchmark
- integration: rewind restores the camera state across a zone change
- camera2d.Adjuster passes the view by value (no per-frame allocation), D304 updated
- modules/camera/module.mk: test-camera in CI, bench-camera
2026-10-21 01:32:28 +03:00

208 lines
6.1 KiB
Go

package camera
import (
"testing"
"aego/core/mathx"
"aego/render/view"
"aego/scene"
"aego/schema"
)
const step = float32(1) / 60
func newScene(t *testing.T) *scene.Scene {
t.Helper()
types := scene.NewTypes(schema.NewRegistry())
if err := schema.RegisterEnum[TimeDomain](types.Schema(), "camera.TimeDomain",
schema.EnumValue{Name: "unscaled", Value: int64(Unscaled)},
schema.EnumValue{Name: "game", Value: int64(Game)},
); err != nil {
t.Fatal(err)
}
for _, err := range []error{
scene.RegisterComponent[VirtualCamera2D](types, schema.Name(NameVirtual)),
scene.RegisterComponent[CameraFollow2D](types, schema.Name(NameFollow)),
scene.RegisterComponent[CameraShake2D](types, schema.Name(NameShake)),
scene.RegisterComponent[CameraZone2D](types, schema.Name(NameZone)),
scene.RegisterComponent[CameraBrain2D](types, schema.Name(NameBrain)),
} {
if err != nil {
t.Fatal(err)
}
}
if err := types.Schema().Freeze(); err != nil {
t.Fatal(err)
}
return scene.New(types, nil, scene.Options{})
}
func vcam(s *scene.Scene, name string, at mathx.Vec2, prio int32, blend float32) scene.NodeHandle {
h := s.Create(s.Root(), name)
s.SetPosition(h, at)
vc := scene.Add[VirtualCamera2D](s, h)
vc.Priority, vc.BlendTime = prio, blend
return h
}
func baseView() view.View {
return view.View{Viewport: mathx.Rect{W: 1, H: 1}, TargetW: 960, TargetH: 540, PixelScale: 1,
Camera: view.Camera2D{Zoom: 32}, Scale: view.ScaleFitVirtual, Virtual: mathx.Vec2{X: 960, Y: 540}}
}
func frame(svc *Service, cam scene.NodeHandle) view.View {
svc.Update(step, step)
v := baseView()
return svc.Adjust(cam, v)
}
func TestPriorityBlendAndRelease(t *testing.T) {
s := newScene(t)
cam := s.Create(s.Root(), "cam")
wide := vcam(s, "wide", mathx.Vec2{}, 0, 0.5)
close := vcam(s, "close", mathx.Vec2{X: 10}, -1, 0.5)
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
if v := frame(svc, cam); v.Camera.Position != (mathx.Vec2{}) || svc.Active("") != wide {
t.Fatalf("wide camera drives the view: %v", v.Camera.Position)
}
vc, _ := scene.Get[VirtualCamera2D](s, close)
vc.Priority = 5
mid := frame(svc, cam)
if svc.Active("") != close || mid.Camera.Position.X <= 0 || mid.Camera.Position.X >= 10 {
t.Fatalf("switch blends: active %v, x %v", svc.Active(""), mid.Camera.Position.X)
}
var v view.View
for range 40 {
v = frame(svc, cam)
}
if v.Camera.Position.X != 10 {
t.Fatalf("blend ends on the new camera: %v", v.Camera.Position.X)
}
if k, err := svc.BlendTo(wide, 0, true); err != nil || k != 0 {
t.Fatalf("cut has no blend: %v %v", k, err)
}
if v = frame(svc, cam); svc.Active("") != wide || v.Camera.Position.X != 0 {
t.Fatalf("cut overrides priority: %v", v.Camera.Position.X)
}
if svc.Release("", 0) == 0 || svc.Active("") != close {
t.Fatal("release blends back to the highest priority")
}
}
func TestZoneLiftsCamera(t *testing.T) {
s := newScene(t)
cam := s.Create(s.Root(), "cam")
target := s.Create(s.Root(), "target")
base := vcam(s, "base", mathx.Vec2{}, 0, 0)
zn := vcam(s, "zone", mathx.Vec2{X: 20}, -1, 0)
z := scene.Add[CameraZone2D](s, zn)
*z = DefaultZone()
z.Target = scene.RefTo(s, target)
z.Zoom = 50
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
frame(svc, cam)
if svc.Active("") != base {
t.Fatal("outside the zone the base camera drives")
}
s.SetPosition(target, mathx.Vec2{X: 20})
s.Flush()
s.UpdateTransforms()
if v := frame(svc, cam); svc.Active("") != zn || v.Camera.Zoom != 50 {
t.Fatalf("inside the zone its camera drives with its zoom: %v", v.Camera.Zoom)
}
s.SetPosition(target, mathx.Vec2{X: 40})
s.Flush()
s.UpdateTransforms()
if frame(svc, cam); svc.Active("") != base {
t.Fatal("leaving the zone hands the view back")
}
if vc, _ := scene.Get[VirtualCamera2D](s, zn); vc.Priority != -1 {
t.Fatal("zones never write camera fields")
}
}
func TestOutputsAreIndependent(t *testing.T) {
s := newScene(t)
main := s.Create(s.Root(), "main")
mini := s.Create(s.Root(), "mini")
scene.Add[CameraBrain2D](s, mini).Output = "map"
vcam(s, "a", mathx.Vec2{X: 1}, 0, 0)
m := vcam(s, "m", mathx.Vec2{X: 7}, 0, 0)
vm, _ := scene.Get[VirtualCamera2D](s, m)
vm.Output = "map"
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc.Update(step, step)
a, b := baseView(), baseView()
a, b = svc.Adjust(main, a), svc.Adjust(mini, b)
if a.Camera.Position.X != 1 || b.Camera.Position.X != 7 {
t.Fatalf("each Camera 2D shows its output: %v %v", a.Camera.Position.X, b.Camera.Position.X)
}
svc.Shake("map", 2)
if svc.Trauma("map") != 1 || svc.Trauma("") != 0 {
t.Fatal("trauma is per output and capped at 1")
}
}
func TestShakeFadesAndRespectsReducedMotion(t *testing.T) {
s := newScene(t)
cam := s.Create(s.Root(), "cam")
vcam(s, "a", mathx.Vec2{}, 0, 0)
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
svc.Shake("", 0.6)
v := frame(svc, cam)
if v.Camera.Position == (mathx.Vec2{}) {
t.Fatal("trauma moves the view")
}
if svc.Logical("").Position != (mathx.Vec2{}) {
t.Fatal("the logical pose has no shake")
}
for range 30 {
svc.Update(step, step)
}
if svc.Trauma("") != 0 {
t.Fatalf("trauma fades by Decay per second: %v", svc.Trauma(""))
}
svc.Shake("", 1)
svc.reduced = true
v = baseView()
svc.ticks++
v = svc.Adjust(cam, v)
if v.Camera.Position != (mathx.Vec2{}) {
t.Fatal("reduced motion turns shake off")
}
}
func TestFollowSnapsOnTeleport(t *testing.T) {
s := newScene(t)
cam := s.Create(s.Root(), "cam")
target := s.Create(s.Root(), "target")
h := vcam(s, "f", mathx.Vec2{}, 0, 0)
f := scene.Add[CameraFollow2D](s, h)
*f = DefaultFollow()
f.Target = scene.RefTo(s, target)
s.Flush()
s.UpdateTransforms()
svc := NewService(s, nil)
frame(svc, cam)
s.SetPosition(target, mathx.Vec2{X: 100})
s.Flush()
s.UpdateTransforms()
if v := frame(svc, cam); v.Camera.Position.X != 100 {
t.Fatalf("a jump over Snap Distance moves the camera at once: %v", v.Camera.Position.X)
}
s.SetPosition(target, mathx.Vec2{X: 103})
s.Flush()
s.UpdateTransforms()
if v := frame(svc, cam); v.Camera.Position.X <= 100 || v.Camera.Position.X >= 103 {
t.Fatalf("a short move is damped: %v", v.Camera.Position.X)
}
}