235 lines
10 KiB
Go
235 lines
10 KiB
Go
package camera
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"aego/core/curve"
|
|
"aego/core/mathx"
|
|
"aego/engine/module"
|
|
"aego/engine/problems"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
"aego/script/bind"
|
|
)
|
|
|
|
const (
|
|
NameVirtual = "camera.VirtualCamera2D"
|
|
NameFollow = "camera.CameraFollow2D"
|
|
NameShake = "camera.CameraShake2D"
|
|
NameZone = "camera.CameraZone2D"
|
|
NameBrain = "camera.CameraBrain2D"
|
|
Category = "Camera"
|
|
|
|
cameraComponent = "engine.Camera2D"
|
|
|
|
CodeNoVirtual = "CAM001"
|
|
CodeNoCamera = "CAM002"
|
|
CodeEmptyRect = "CAM003"
|
|
CodeNoTarget = "CAM004"
|
|
)
|
|
|
|
type TimeDomain uint8
|
|
|
|
const (
|
|
Unscaled TimeDomain = iota
|
|
Game
|
|
)
|
|
|
|
type VirtualCamera2D struct {
|
|
Priority int32 `aego:"priority"`
|
|
Output string `aego:"output"`
|
|
Zoom float32 `aego:"zoom,min=0"`
|
|
BlendTime float32 `aego:"blendTime,min=0"`
|
|
BlendCurve curve.Curve `aego:"blendCurve"`
|
|
TimeDomain TimeDomain `aego:"timeDomain"`
|
|
UseBounds bool `aego:"useBounds"`
|
|
Bounds mathx.Rect `aego:"bounds"`
|
|
SnapToPixels bool `aego:"snapToPixels"`
|
|
}
|
|
|
|
type CameraFollow2D struct {
|
|
Target scene.NodeRef `aego:"target"`
|
|
Deadzone mathx.Rect `aego:"deadzone"`
|
|
Damping mathx.Vec2 `aego:"damping"`
|
|
Lookahead mathx.Vec2 `aego:"lookahead"`
|
|
MaxLookahead mathx.Vec2 `aego:"maxLookahead"`
|
|
LookaheadSmoothing float32 `aego:"lookaheadSmoothing,min=0"`
|
|
SnapDistance float32 `aego:"snapDistance,min=0"`
|
|
}
|
|
|
|
type CameraShake2D struct {
|
|
Decay float32 `aego:"decay,min=0"`
|
|
MaxOffset mathx.Vec2 `aego:"maxOffset"`
|
|
MaxAngle float32 `aego:"maxAngle,min=0"`
|
|
Frequency float32 `aego:"frequency,min=0"`
|
|
Exponent float32 `aego:"exponent,min=1"`
|
|
}
|
|
|
|
type CameraZone2D struct {
|
|
Target scene.NodeRef `aego:"target"`
|
|
Camera scene.NodeRef `aego:"camera"`
|
|
Priority int32 `aego:"priority"`
|
|
Zoom float32 `aego:"zoom,min=0"`
|
|
UseBounds bool `aego:"useBounds"`
|
|
Bounds mathx.Rect `aego:"bounds"`
|
|
Area mathx.Rect `aego:"area"`
|
|
}
|
|
|
|
type CameraBrain2D struct {
|
|
Output string `aego:"output"`
|
|
}
|
|
|
|
func DefaultVirtual() VirtualCamera2D {
|
|
return VirtualCamera2D{BlendTime: 0.5, BlendCurve: curve.FromEasing(curve.InOutQuad)}
|
|
}
|
|
|
|
func DefaultFollow() CameraFollow2D {
|
|
return CameraFollow2D{
|
|
Deadzone: mathx.Rect{X: 0.4, Y: 0.4, W: 0.2, H: 0.2},
|
|
Damping: mathx.Vec2{X: 6, Y: 6},
|
|
Lookahead: mathx.Vec2{X: 0.25},
|
|
MaxLookahead: mathx.Vec2{X: 3, Y: 1},
|
|
LookaheadSmoothing: 3,
|
|
SnapDistance: 20,
|
|
}
|
|
}
|
|
|
|
func DefaultShake() CameraShake2D {
|
|
return CameraShake2D{Decay: 1.5, MaxOffset: mathx.Vec2{X: 0.5, Y: 0.5}, MaxAngle: 0.05, Frequency: 25, Exponent: 2}
|
|
}
|
|
|
|
func DefaultZone() CameraZone2D {
|
|
return CameraZone2D{Priority: 10, Area: mathx.Rect{X: -4, Y: -3, W: 8, H: 6}}
|
|
}
|
|
|
|
func diag(code string, sev problems.Severity, msg string) problems.Diagnostic {
|
|
return problems.Diagnostic{Code: code, Severity: sev, Message: msg}
|
|
}
|
|
|
|
func empty(r mathx.Rect) bool {
|
|
return r.W <= 0 || r.H <= 0
|
|
}
|
|
|
|
func validateVirtual(_ module.ValidateContext, vc *VirtualCamera2D) []problems.Diagnostic {
|
|
if vc.UseBounds && empty(vc.Bounds) {
|
|
return []problems.Diagnostic{diag(CodeEmptyRect, problems.Warning, "Virtual Camera 2D: Bounds has no area, the camera is not clamped")}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateFollow(ctx module.ValidateContext, f *CameraFollow2D) []problems.Diagnostic {
|
|
var out []problems.Diagnostic
|
|
if !scene.Has[VirtualCamera2D](ctx.Scene, ctx.Node) {
|
|
out = append(out, diag(CodeNoVirtual, problems.Warning, "Camera Follow 2D needs a Virtual Camera 2D on the same node"))
|
|
}
|
|
if f.Target.IsZero() {
|
|
out = append(out, diag(CodeNoTarget, problems.Warning, "Camera Follow 2D has no Target"))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func validateShake(ctx module.ValidateContext, _ *CameraShake2D) []problems.Diagnostic {
|
|
if !scene.Has[VirtualCamera2D](ctx.Scene, ctx.Node) {
|
|
return []problems.Diagnostic{diag(CodeNoVirtual, problems.Warning, "Camera Shake 2D needs a Virtual Camera 2D on the same node")}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateZone(ctx module.ValidateContext, z *CameraZone2D) []problems.Diagnostic {
|
|
var out []problems.Diagnostic
|
|
if z.Camera.IsZero() && !scene.Has[VirtualCamera2D](ctx.Scene, ctx.Node) {
|
|
out = append(out, diag(CodeNoCamera, problems.Warning, "Camera Zone 2D needs a Camera or a Virtual Camera 2D on the same node"))
|
|
}
|
|
if empty(z.Area) {
|
|
out = append(out, diag(CodeEmptyRect, problems.Warning, "Camera Zone 2D: Area has no size, the zone never triggers"))
|
|
}
|
|
if z.UseBounds && empty(z.Bounds) {
|
|
out = append(out, diag(CodeEmptyRect, problems.Warning, "Camera Zone 2D: Bounds has no area"))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func followCamera(s *scene.Scene, h scene.NodeHandle) {
|
|
reg := s.Registry()
|
|
for _, name := range [...]string{cameraComponent, NameVirtual, NameFollow} {
|
|
if td, ok := reg.ByName(name); ok {
|
|
module.Ensure(s, h, td)
|
|
}
|
|
}
|
|
}
|
|
|
|
func registerTypes(r *module.Registry) error {
|
|
if err := module.Enum[TimeDomain](r, "camera.TimeDomain",
|
|
schema.EnumValue{Name: "unscaled", Value: int64(Unscaled)},
|
|
schema.EnumValue{Name: "game", Value: int64(Game)},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
space := schema.Space(schema.Space2D)
|
|
vc := module.Component[VirtualCamera2D](r, schema.Name(NameVirtual), space,
|
|
schema.Doc("A camera pose the brain can pick. The highest priority wins; switching blends from the current view."),
|
|
schema.FieldDoc("priority", "The enabled virtual camera with the highest priority drives its output."),
|
|
schema.FieldDoc("output", "Output this camera drives. Empty is the main output; a Camera 2D picks another one with Camera Brain 2D."),
|
|
schema.FieldDoc("zoom", "Pixels per world unit while this camera is active. 0 keeps the zoom of the Camera 2D."),
|
|
schema.FieldDoc("blendTime", "Seconds to blend into this camera when it becomes active. 0 cuts."),
|
|
schema.FieldDoc("blendCurve", "Shape of the blend; its time range is stretched over Blend Time."),
|
|
schema.FieldDoc("timeDomain", "unscaled blends and shakes in real time, also during hit-stop and pause; game follows the time scale."),
|
|
schema.FieldDoc("useBounds", "Keeps the visible area inside Bounds."),
|
|
schema.FieldDoc("bounds", "World rectangle the view stays inside. A smaller area centres the view on it."),
|
|
schema.FieldDoc("snapToPixels", "Rounds the final position to whole screen pixels. pixelPerfect cameras already do this."),
|
|
).Category(Category).Display("Virtual Camera 2D").Icon("comp.vcam").NodeIcon(90).Default(DefaultVirtual).Validate(validateVirtual)
|
|
vc.Method0("IsActive", func(c *bind.Ctx, _ *VirtualCamera2D) bool {
|
|
s := live(c)
|
|
return s != nil && s.IsActive(c.Recv)
|
|
}, bind.Doc("IsActive reports whether this virtual camera drives its output now."))
|
|
|
|
follow := module.Component[CameraFollow2D](r, schema.Name(NameFollow), space,
|
|
schema.Doc("Moves its virtual camera after Target, with a deadzone, damping and lookahead."),
|
|
schema.FieldDoc("target", "Node to follow; its interpolated position is used."),
|
|
schema.FieldDoc("deadzone", "Area of the view, in 0..1 of its width and height, where the target moves without moving the camera."),
|
|
schema.FieldDoc("damping", "How fast the camera catches up, per second on each axis. 0 follows rigidly."),
|
|
schema.FieldDoc("lookahead", "Seconds of target velocity the camera looks ahead on each axis."),
|
|
schema.FieldDoc("maxLookahead", "Largest lookahead in world units on each axis. 0 is unlimited."),
|
|
schema.FieldDoc("lookaheadSmoothing", "How fast the lookahead changes, per second. 0 changes at once."),
|
|
schema.FieldDoc("snapDistance", "A target jump longer than this, in world units, moves the camera at once. 0 never snaps."),
|
|
).Requires(NameVirtual).Category(Category).Display("Camera Follow 2D").Icon("comp.follow").Default(DefaultFollow).Validate(validateFollow)
|
|
follow.Proc0("Snap", func(c *bind.Ctx, _ *CameraFollow2D) {
|
|
if s := live(c); s != nil {
|
|
s.Snap(c.Recv)
|
|
}
|
|
}, bind.Doc("Snap moves the camera onto the target at once, for example after a teleport."))
|
|
|
|
shake := module.Component[CameraShake2D](r, schema.Name(NameShake), space,
|
|
schema.Doc("Tunes the shake of its virtual camera. Shake grows with trauma and fades over time."),
|
|
schema.FieldDoc("decay", "Trauma lost per second."),
|
|
schema.FieldDoc("maxOffset", "Largest offset in world units at full trauma."),
|
|
schema.FieldDoc("maxAngle", "Largest rotation in radians at full trauma."),
|
|
schema.FieldDoc("frequency", "How fast the shake changes, in noise steps per second."),
|
|
schema.FieldDoc("exponent", "Shake is trauma to this power: 2 keeps small hits subtle."),
|
|
).Requires(NameVirtual).Category(Category).Display("Camera Shake 2D").Icon("comp.shake").Default(DefaultShake).Validate(validateShake)
|
|
shake.Proc1("Add", func(c *bind.Ctx, _ *CameraShake2D, t float32) {
|
|
if s := live(c); s != nil {
|
|
s.ShakeNode(c.Recv, t)
|
|
}
|
|
}, bind.Doc("Add adds trauma to the output of this camera; trauma is capped at 1."), bind.Params("trauma"))
|
|
|
|
zone := module.Component[CameraZone2D](r, schema.Name(NameZone), space,
|
|
schema.Doc("While Target is inside Area, Camera gets Priority and the zone's zoom and bounds."),
|
|
schema.FieldDoc("target", "Node that triggers the zone. Empty uses the follow target of the zone's camera."),
|
|
schema.FieldDoc("camera", "Virtual camera the zone activates. Empty uses the one on this node."),
|
|
schema.FieldDoc("priority", "Priority of the camera while the target is inside."),
|
|
schema.FieldDoc("zoom", "Zoom while the zone is in effect. 0 keeps the camera's zoom."),
|
|
schema.FieldDoc("useBounds", "Replaces the camera's bounds while the zone is in effect."),
|
|
schema.FieldDoc("bounds", "World rectangle used instead of the camera's bounds."),
|
|
schema.FieldDoc("area", "Trigger rectangle in this node's space."),
|
|
).Category(Category).Display("Camera Zone 2D").Icon("comp.zone").Default(DefaultZone).Validate(validateZone)
|
|
|
|
brain := module.Component[CameraBrain2D](r, schema.Name(NameBrain), space,
|
|
schema.Doc("Picks which output of virtual cameras drives this Camera 2D. Without it the camera shows the main output."),
|
|
schema.FieldDoc("output", "Output name; virtual cameras with the same Output drive this camera."),
|
|
).Category(Category).Display("Camera Brain 2D").Icon("comp.camera2d")
|
|
|
|
r.Template(schema.TemplateDesc{ID: ID + ".follow", Name: "Follow Camera", Icon: "comp.follow", Category: Category, Space: schema.Space2D, Setup: followCamera})
|
|
return errors.Join(vc.Err(), follow.Err(), shake.Err(), zone.Err(), brain.Err())
|
|
}
|