Compare commits
3
Commits
c4643dc20c
...
60556df87c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60556df87c | ||
|
|
67586b178c | ||
|
|
2f8ea7af98 |
+9
-1
@@ -524,7 +524,7 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
|
||||
| D303 | Docs delivery. The landing page embeds the web demo as a click-to-play iframe over its poster and starts it at once for a visitor who has played it before (a localStorage mark). The Try it loader compiles `aego-docs.wasm` straight from the network response, so the browser keeps both the file and its compiled code in the HTTP cache, and counts a copy of the first download for a progress bar against the uncompressed size written into the page; a localStorage mark of the versioned URL tells later pages the runtime was started before, and they start the first Try it block that scrolls into view without waiting for a click (the Cache API was tried and dropped: responses rebuilt from it lose the compiled-code cache, and probing the HTTP cache with `only-if-cached` holds its lock on the same URL); the block shows the scene as a tree (`sceneio.Outline`, node → components → typed fields linked to their docs, values updated from the runtime and changed ones marked) with Open in editor (downloads the `.aescene`) and Copy as .aescene. The docs runtime shows the `physics.shapes` and `physics.joints` debug draw channels, so colliders and joints are visible without art. nginx serves every `.wasm` with a brotli or gzip copy made at image build, `?v=` URLs as `immutable` for a year, HTML and JSON for 60 seconds (`no-cache` kept the edge proxy in front of the site from storing new pages, so it served stale ones), and `install.sh` as text; the web export preloads its `game.wasm`. Amended 2026-10-20: the site sits behind the edge proxy at 185.170.196.188, whose route to mainserv is slow (~0.2 MB/s) and drops, so the edge serves aego from its own copy (`deploy/edge`): the image carries `__edge/site.tar.gz` and `__edge/version`, the edge timer `aego-wasm-sync` fetches a new site archive with resumable downloads and switches `/var/www/aego-site/current` atomically, and copies each wasm into `/var/www/aego-wasm/<version>/`; anything not there yet goes to mainserv through the proxy, so a new `?v=` never gets old bytes. Pages show the commit in the header, and styles, scripts and the icon sprite are loaded as `?v=<hash of their contents>`, so a new revision reaches every browser within the 60 s HTML lifetime. |
|
||||
| D304 | Camera adjustment is a pull contract. `contracts/camera2d` declares `Adjuster` under `camera2d.adjust`. `render2d.cameras` builds the view of every active Camera 2D from its interpolated pose, then passes it through `view = Adjust(node, view)` when the play world has the service; the value form keeps the per-frame path free of allocations. Rendering never names the camera module and the camera module never names render systems (D204); the contract may import `render/view` (a depcheck exception). An output is evaluated once per `camera.update` tick; a second Camera 2D on the same output reuses the pose. The edit world has no adjuster, fixed views are never adjusted. The module ID `camera` collides with the scene view state field `"camera"`, which is excepted in depcheck. |
|
||||
| D305 | Virtual cameras, priorities, blends and zones. Each output picks the enabled virtual camera with the highest priority; ties go to the lower node handle index, then zones over plain cameras, the later zone over the earlier, and BlendTo/Cut over everything until Release. Zones are derived each tick from the exact world position of the target (not the interpolated one), so entering is deterministic; they never write component fields. A switch blends from the current logical pose, zoom in log space; a replaced blend completes with finished=false. Blends are awaitable handles `aego/camera.Blend`. Outputs are chosen per Camera 2D by `camera.CameraBrain2D`; without it the camera shows the main output. |
|
||||
| D306 | Follow, shake, accessibility and time. Follow reads the interpolated target pose and runs in game time with exponential, rate-independent damping, a deadzone in view fractions, lookahead in seconds of velocity and snapping on teleports. Shake is trauma^exponent times deterministic gradient noise seeded by the output name; it never touches node transforms or the world RNG. Accessibility provides `camera2d.accessibility` with its setting IDs: Screen Shake scales shake, Reduce Motion disables shake and lookahead. Blends and shake follow the VC time domain (unscaled by default). The rewind hash covers active cameras, overrides, zones and game-domain blends and trauma; follow is captured but not hashed, unscaled blends finish on restore. |
|
||||
| D306 | Follow, shake, accessibility and time. Follow reads the interpolated target pose and runs in game time with exponential, rate-independent damping, a deadzone in view fractions, lookahead in seconds of velocity and snapping on teleports. Shake is trauma^exponent times deterministic gradient noise seeded by the output name; it never touches node transforms or the world RNG. Accessibility provides `camera2d.accessibility` with its setting IDs: Screen Shake scales shake, Reduce Motion disables shake and lookahead. Blends and shake follow the VC time domain (unscaled by default). The rewind hash covers active cameras, overrides, zones and game-domain blends and trauma; follow is captured but not hashed, unscaled blends finish on restore. A blend that starts on a tick nothing drew (a seek replays ticks undrawn) takes its start pose from the previous virtual camera at the next draw, not from the stale last drawn pose. |
|
||||
### 3.22 Stage 10b1 — Audio foundation
|
||||
|
||||
| ID | Decision |
|
||||
@@ -552,6 +552,14 @@ The Stage 8 specification numbered these decisions D153, D155, D158, D159, D168
|
||||
|---|---|
|
||||
| D327 | Desktop-first onboarding. Installing, creating and opening projects, testing and exporting need no terminal on Windows, macOS and Linux; the CLI stays for agents and CI. Every desktop entry point (the Windows shortcut and file associations for `.aego`, `.aescene`, `.aeprefab` and folders, the macOS app's document types, the Linux AppImage and the `.desktop` file from install.sh) starts the same `edit.Start` as `aego edit`: without a project it opens the launcher, a file argument opens its project and, for a scene or prefab, that file (`-open`, passed through the supervisor), a native project goes to the supervisor when Go is found and otherwise opens for scripts with a status-bar note. GUI builds write logs to the user cache (`aego/logs`, last ten) and show fatal errors in a message box; child processes are started without console windows (`app/proc`), and the supervised editor is built with `-H=windowsgui`. Native dialogs are `platform.FileDialogs` on the window, implemented with SDL's folder and file dialogs and delivered through a registered SDL event; headless windows have none and the UI falls back to a text field. The launcher creates projects in `Documents/Aego Projects` by default (XDG documents on Linux), offers native code only with Go and engine sources, opens dropped folders and `project.aego`, and has Open, Show in the file manager and Remove on recent projects. Once a day it asks the Gitea API for the latest release and shows a banner linking the download page; it never updates by itself and the check can be turned off (`UserConfigDir/aego/update`). Switching projects from the editor (New, Open, Open Recent, Close Project, and dropping another project's folder) starts the original entry point again (`AEGO_EDITOR_EXE`, `AEGO_EDITOR_VERB`, `$APPIMAGE`) without the supervised flag and quits, after asking about unsaved documents. Play → Run Script Tests runs `aego test` in-process with results in Console; the Export panel takes an output folder and can open or run the result, serving web builds on 127.0.0.1 with COOP/COEP. The home and install pages carry download buttons that gendocs renders at site build time from the latest Gitea release (`site.FetchRelease`, `downloadBlock`): one list item per file that the release has, and `site.js` turns the main button into the visitor's system, or into a link to the install page when the release has no file for it. The app icon (D301) is the window icon of the launcher and the editor through `platform.SetAppIcon`, set in `edit.Start` before the first window. `edit.Start` writes a boot log to `<user cache>/aego/logs/boot.log` (the previous run is kept as `boot.prev.log`): version, executable, arguments, working directory, Go, then each startup step (SDL init, window, GL version, launcher, project, missing Go). A failed OpenGL 3.3 core context probes a default context and names what the driver has (`gl: … driver has GDI Generic 1.1.0`). A start error or panic shows a message box with the log path; on Windows through `MessageBoxW` (`platform.ShowFatal`, usable before and after `SDL_Init`), elsewhere through the SDL alert. |
|
||||
|
||||
### 3.25 Stage 10c — Gameplay kit
|
||||
|
||||
| ID | Decision |
|
||||
|---|---|
|
||||
| D328 | Shape casts. `physics/world` casts a circle, box, capsule or every non-trigger convex shape of a body along a translation (`CastCircle`, `CastBox`, `CastCapsule`, `CastBody`, which starts from the body's pose moved by an offset) with GJK distance between two polygon-with-radius cores and conservative advancement; a hit stops a quarter of linear slop before contact, reports the fraction of the move, the contact point on the obstacle and the obstacle's normal pointing back at the mover. A cast that starts in contact returns `Initial` with fraction 0 and the overlap depth; when the cores intersect, depth, normal and point come from the SAT manifold. `Penetration` lists the overlaps of a body's shapes with their depth and push-out normal. Casts skip triggers unless asked, skip the mover's own body, honour one-sided chains (a mover behind the chain is not blocked) and, with `SkipDynamic`, ignore dynamic bodies so a kinematic mover pushes them. `contracts/physics2d.Queries` exposes the casts by node together with `BodyPose`, `BodyVelocity`, `MoveKinematic`, `Teleport`, `ApplyImpulse` and `Gravity`; `QueryFilter.ExcludeBody` excludes the node's body and all its colliders. Casts allocate nothing. |
|
||||
| D312 | Motion2D (`modules/gameplay`) is a cast-based kinematic controller over `contracts/physics2d`: each fixed step in `gameplay.motion` (PrePhysics, after `script.fixed`) it turns intents (`Move`, `MoveDir`, `Jump`, `JumpReleased`, `Dash`, `AddVelocity`, `Stop`, `Teleport`; the last Move stays, a Jump is one request) into a velocity, pushes the body out of overlaps (`Penetration`), slides along up to MaxSlides surfaces with `CastBody` from the moved pose, keeping SafeMargin, classifies each contact against UpDirection and MaxSlope as floor, wall or ceiling, climbs steps up to StepHeight, snaps to the floor by SnapToGround while grounded and moves the body with `MoveKinematic`, so physics carries it and pushes dynamic bodies (`PushBodies` casts through them). Grounded movement follows the floor tangent without gravity, so slopes below MaxSlope hold. Coyote time, jump buffer, jump cut, air control, dash and moving platforms (`BodyVelocity` of the ground body; PlatformOnLeave keep, drop or add) are part of the step. Signals `Landed(speed)`, `LeftGround`, `Jumped`, `HitWall(normal)`, `HitCeiling` are emitted in `gameplay.motion_post` (PostPhysics, before `script.events_fixed`). The controller state lives in an unexported field of the component, so rewind keys and pooled spawns carry it with the scene. GMP001: the body is not kinematic (logged once); GMP002: a platformer without gravity and Jump Speed. |
|
||||
| D329 | Motion2D owns no Go map or per-step allocation: one service per play world walks the components with a stored callback, casts reuse the physics query buffers. |
|
||||
|
||||
## 4. Dependency rules
|
||||
|
||||
The import graph is acyclic. The rules below are enforced by `tools/depcheck`.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"aego/modules/dataasset"
|
||||
"aego/modules/debug"
|
||||
"aego/modules/display"
|
||||
"aego/modules/gameplay"
|
||||
inputmod "aego/modules/input"
|
||||
"aego/modules/physics"
|
||||
"aego/modules/render2d"
|
||||
@@ -76,6 +77,10 @@ func cameraEntry() assembly.Entry {
|
||||
return assembly.Entry{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }}
|
||||
}
|
||||
|
||||
func gameplayEntry() assembly.Entry {
|
||||
return assembly.Entry{ID: gameplay.ID, Requires: []string{physics.ID, scripting.ID}, New: func(assembly.Options) module.Module { return gameplay.New() }}
|
||||
}
|
||||
|
||||
func dataEntry() assembly.Entry {
|
||||
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
|
||||
}
|
||||
@@ -90,5 +95,5 @@ func savegameEntry() assembly.Entry {
|
||||
|
||||
func Default() assembly.Catalog {
|
||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
|
||||
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), spawnEntry(), savegameEntry()}
|
||||
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), gameplayEntry(), spawnEntry(), savegameEntry()}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"aego/modules/audio"
|
||||
"aego/modules/camera"
|
||||
"aego/modules/debug"
|
||||
"aego/modules/gameplay"
|
||||
inputmod "aego/modules/input"
|
||||
"aego/modules/physics"
|
||||
"aego/modules/render2d"
|
||||
@@ -31,6 +32,7 @@ func Catalog() assembly.Catalog {
|
||||
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
|
||||
{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }},
|
||||
{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }},
|
||||
{ID: gameplay.ID, Requires: []string{physics.ID, scripting.ID}, New: func(assembly.Options) module.Module { return gameplay.New() }},
|
||||
{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }},
|
||||
{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }},
|
||||
{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }},
|
||||
|
||||
@@ -10,7 +10,9 @@ const ServiceID = "physics2d.queries"
|
||||
type QueryFilter struct {
|
||||
LayerMask uint32
|
||||
IncludeTriggers bool
|
||||
SkipDynamic bool
|
||||
ExcludeNode scene.NodeHandle
|
||||
ExcludeBody scene.NodeHandle
|
||||
}
|
||||
|
||||
type RayHit struct {
|
||||
@@ -21,6 +23,16 @@ type RayHit struct {
|
||||
ColliderNode scene.NodeHandle
|
||||
}
|
||||
|
||||
type CastHit struct {
|
||||
Point mathx.Vec2
|
||||
Normal mathx.Vec2
|
||||
Fraction float32
|
||||
Depth float32
|
||||
Node scene.NodeHandle
|
||||
ColliderNode scene.NodeHandle
|
||||
Initial bool
|
||||
}
|
||||
|
||||
type Queries interface {
|
||||
Raycast(origin, dir mathx.Vec2, maxDist float32, f QueryFilter) (RayHit, bool)
|
||||
RaycastAll(origin, dir mathx.Vec2, maxDist float32, f QueryFilter, out []RayHit) int
|
||||
@@ -28,4 +40,15 @@ type Queries interface {
|
||||
OverlapCircle(c mathx.Vec2, r float32, f QueryFilter, out []scene.NodeHandle) int
|
||||
OverlapBox(c, half mathx.Vec2, angle float32, f QueryFilter, out []scene.NodeHandle) int
|
||||
BodyAt(p mathx.Vec2, f QueryFilter) (scene.NodeHandle, bool)
|
||||
CastCircle(c mathx.Vec2, r float32, delta mathx.Vec2, f QueryFilter) (CastHit, bool)
|
||||
CastBox(c, half mathx.Vec2, angle float32, delta mathx.Vec2, f QueryFilter) (CastHit, bool)
|
||||
CastCapsule(a, b mathx.Vec2, r float32, delta mathx.Vec2, f QueryFilter) (CastHit, bool)
|
||||
CastBody(body scene.NodeHandle, offset, delta mathx.Vec2, f QueryFilter) (CastHit, bool)
|
||||
Penetration(body scene.NodeHandle, f QueryFilter, out []CastHit) int
|
||||
BodyPose(body scene.NodeHandle) (mathx.Vec2, float32, bool)
|
||||
BodyVelocity(body scene.NodeHandle) (mathx.Vec2, float32, bool)
|
||||
MoveKinematic(body scene.NodeHandle, p mathx.Vec2, angle, dt float32) error
|
||||
Teleport(body scene.NodeHandle, p mathx.Vec2, angle float32) error
|
||||
ApplyImpulse(body scene.NodeHandle, j mathx.Vec2) error
|
||||
Gravity() mathx.Vec2
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `cat.audio` | engine | 16 | Documentation: Audio group and module | ok |
|
||||
| `cat.camera` | engine | 24 | New Project: template 2.5d | ok |
|
||||
| `cat.game` | engine | 16 | Inspector and Add Component: components of a project module without an icon of their own | ok |
|
||||
| `cat.gameplay` | engine | 16 | — | unused |
|
||||
| `cat.node` | engine | 24 | New Project: template empty; Search: node results | ok |
|
||||
| `cat.physics` | engine | 16 | template menu Physics | ok |
|
||||
| `cat.rendering` | engine | 24 | New Project: template 2d; Search: mesh assets | ok |
|
||||
@@ -75,6 +76,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `file.table` | engine | 24 | Assets: table tiles | ok |
|
||||
| `file.texture` | engine | 24 | Assets: texture tiles | ok |
|
||||
| `file.unknown` | engine | 24 | Assets: tiles of other kinds | ok |
|
||||
| `gameplay.motion` | engine | 16 | component gameplay.Motion2D | ok |
|
||||
| `help.about` | engine | 16 | Help → About Aego | ok |
|
||||
| `help.book` | engine | 16 | Documentation tab | ok |
|
||||
| `log.error` | engine | 16 | Console: errors | ok |
|
||||
|
||||
@@ -3,3 +3,10 @@ package context
|
||||
import "aego/core/ids"
|
||||
|
||||
type AssetView func(*Editor, ids.AssetID)
|
||||
|
||||
// AssetPlayer auditions assets of one artifact kind or data type, such as sounds in an asset picker.
|
||||
type AssetPlayer struct {
|
||||
Play func(*Editor, ids.AssetID) error
|
||||
Stop func(*Editor)
|
||||
Playing func(*Editor) ids.AssetID
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ type Editor struct {
|
||||
Vectors []VectorLayer
|
||||
Profiler []ProfilerSection
|
||||
AssetViews map[string]AssetView
|
||||
AssetPlayers map[string]AssetPlayer
|
||||
Overlay Overlays
|
||||
ShowSettings func(page string)
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import (
|
||||
|
||||
const peaksExt = ".peaks"
|
||||
|
||||
type peakKey struct {
|
||||
id ids.AssetID
|
||||
cols int
|
||||
}
|
||||
|
||||
type peakItem struct {
|
||||
key string
|
||||
data []float32
|
||||
@@ -35,9 +40,10 @@ func (s *Service) Peaks(id ids.AssetID, cols int) ([]float32, bool) {
|
||||
}
|
||||
key += "-" + strconv.Itoa(cols)
|
||||
if s.peaks == nil {
|
||||
s.peaks = map[ids.AssetID]*peakItem{}
|
||||
s.peaks = map[peakKey]*peakItem{}
|
||||
}
|
||||
it := s.peaks[id]
|
||||
pk := peakKey{id, cols}
|
||||
it := s.peaks[pk]
|
||||
if it != nil && it.key == key {
|
||||
return it.data, it.done
|
||||
}
|
||||
@@ -45,7 +51,7 @@ func (s *Service) Peaks(id ids.AssetID, cols int) ([]float32, bool) {
|
||||
it.task.Cancel()
|
||||
}
|
||||
it = &peakItem{key: key}
|
||||
s.peaks[id] = it
|
||||
s.peaks[pk] = it
|
||||
art, _ := e.Artifact(e.Kind)
|
||||
path := filepath.Join(s.dir, key+peaksExt)
|
||||
var out []float32
|
||||
@@ -73,7 +79,7 @@ func (s *Service) Peaks(id ids.AssetID, cols int) ([]float32, bool) {
|
||||
}
|
||||
return writePeaks(path, out)
|
||||
}).Then(func(t *tasks.Task) {
|
||||
if s.peaks[id] != it {
|
||||
if s.peaks[pk] != it {
|
||||
return
|
||||
}
|
||||
it.task, it.done = nil, true
|
||||
|
||||
@@ -76,7 +76,7 @@ type Service struct {
|
||||
d Deps
|
||||
dir string
|
||||
items map[ids.AssetID]*item
|
||||
peaks map[ids.AssetID]*peakItem
|
||||
peaks map[peakKey]*peakItem
|
||||
frame int64
|
||||
gpuOK bool
|
||||
pool *rt.Pool
|
||||
@@ -515,11 +515,14 @@ func downscale(src []byte, w, h, limit int) ([]byte, int, int) {
|
||||
}
|
||||
|
||||
func (s *Service) Forget(id ids.AssetID) {
|
||||
if pk := s.peaks[id]; pk != nil {
|
||||
for k, pk := range s.peaks {
|
||||
if k.id != id {
|
||||
continue
|
||||
}
|
||||
if pk.task != nil {
|
||||
pk.task.Cancel()
|
||||
}
|
||||
delete(s.peaks, id)
|
||||
delete(s.peaks, k)
|
||||
}
|
||||
it, ok := s.items[id]
|
||||
if !ok {
|
||||
|
||||
@@ -36,6 +36,7 @@ type owned struct {
|
||||
gizmos []schema.TypeID
|
||||
inspectors []schema.TypeID
|
||||
assets []string
|
||||
players []string
|
||||
}
|
||||
|
||||
type pending struct {
|
||||
@@ -145,6 +146,9 @@ func (r *Registry) disableNow(id, reason string) {
|
||||
for _, k := range o.assets {
|
||||
delete(ed.AssetViews, k)
|
||||
}
|
||||
for _, k := range o.players {
|
||||
delete(ed.AssetPlayers, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.deps.Workspace != nil {
|
||||
|
||||
@@ -104,3 +104,43 @@ func (r *Registry) AssetInspector(kind string, fn context.AssetView) error {
|
||||
o.assets = append(o.assets, kind)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssetPlayer registers an audition for an artifact kind or a data type's full name.
|
||||
func (r *Registry) AssetPlayer(key string, pl context.AssetPlayer) error {
|
||||
ed := r.deps.Editor
|
||||
ext := r.ext()
|
||||
if ed == nil {
|
||||
return nil
|
||||
}
|
||||
if key == "" || pl.Play == nil || pl.Stop == nil || pl.Playing == nil {
|
||||
return errs.New(errs.EditorPanel, "asset player needs a kind and Play, Stop and Playing functions", log.F("module", ext))
|
||||
}
|
||||
if _, dup := ed.AssetPlayers[key]; dup {
|
||||
return errs.New(errs.EditorPanel, "asset player for "+key+" is already registered", log.F("module", ext))
|
||||
}
|
||||
if ed.AssetPlayers == nil {
|
||||
ed.AssetPlayers = map[string]context.AssetPlayer{}
|
||||
}
|
||||
guard := func(fn func()) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
r.Disable(ext, fmt.Sprint(rec))
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
ed.AssetPlayers[key] = context.AssetPlayer{
|
||||
Play: func(ctx *context.Editor, id ids.AssetID) (err error) {
|
||||
guard(func() { err = pl.Play(ctx, id) })
|
||||
return err
|
||||
},
|
||||
Stop: func(ctx *context.Editor) { guard(func() { pl.Stop(ctx) }) },
|
||||
Playing: func(ctx *context.Editor) (id ids.AssetID) {
|
||||
guard(func() { id = pl.Playing(ctx) })
|
||||
return id
|
||||
},
|
||||
}
|
||||
o := r.own(ext)
|
||||
o.players = append(o.players, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -561,12 +561,15 @@ func (p *Panel) assetTile(a *pipeline.AssetInfo, r mathx.Rect) {
|
||||
}
|
||||
ts := p.tileSize()
|
||||
img := mathx.Rect{X: r.X + (r.W-ts)/2, Y: r.Y + 4, W: ts, H: ts}
|
||||
if reg, ok := p.d.Thumbs.Get(*a); ok {
|
||||
switch reg, ok := p.d.Thumbs.Get(*a); {
|
||||
case a.Kind == asset.ArtAudio:
|
||||
p.audioCard(a, img)
|
||||
case ok:
|
||||
if preview.Image(a.Kind) || a.Kind == asset.ArtPrefab {
|
||||
p.checker(img)
|
||||
}
|
||||
ui.List().Texture(reg, fitRect(img, reg.W, reg.H), mathx.Color{R: 1, G: 1, B: 1, A: 1})
|
||||
} else {
|
||||
default:
|
||||
p.schematic(a, img)
|
||||
}
|
||||
startup := p.d.Project != nil && a.ID == p.d.Project.MainScene
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ package inspector
|
||||
import (
|
||||
"aego/editor/ext/inspect"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -53,6 +54,7 @@ type Deps struct {
|
||||
Schema *schema.Registry
|
||||
Presets *presets.Service
|
||||
AssetView func(ids.AssetID) bool
|
||||
Audition func(id ids.AssetID, toggle bool) (playing, ok bool)
|
||||
}
|
||||
|
||||
type Panel struct {
|
||||
@@ -686,15 +688,9 @@ func (p *Panel) reset(d *document.SceneDocument, td *schema.TypeDescriptor) {
|
||||
|
||||
func (p *Panel) blocked(s *scene.Scene, reg *schema.Registry, td *schema.TypeDescriptor) string {
|
||||
for _, name := range td.Requires {
|
||||
req, ok := reg.ByName(name)
|
||||
if !ok {
|
||||
if _, ok := reg.ByName(name); !ok {
|
||||
return "needs " + name
|
||||
}
|
||||
for _, n := range p.nodes {
|
||||
if _, has := s.GetByType(s.Handle(n), req.ID); !has {
|
||||
return "needs " + p.reg.Meta(req).DisplayName
|
||||
}
|
||||
}
|
||||
}
|
||||
if td.SceneUnique && (len(p.nodes) > 1 || inScene(s, td.ID)) {
|
||||
return "one per scene"
|
||||
@@ -702,6 +698,22 @@ func (p *Panel) blocked(s *scene.Scene, reg *schema.Registry, td *schema.TypeDes
|
||||
return ""
|
||||
}
|
||||
|
||||
// requiredChain lists the components td needs that node n lacks, dependencies first.
|
||||
func (p *Panel) requiredChain(s *scene.Scene, reg *schema.Registry, td *schema.TypeDescriptor, n ids.NodeID, out []*schema.TypeDescriptor) []*schema.TypeDescriptor {
|
||||
for _, name := range td.Requires {
|
||||
req, ok := reg.ByName(name)
|
||||
if !ok || slices.Contains(out, req) {
|
||||
continue
|
||||
}
|
||||
if _, has := s.GetByType(s.Handle(n), req.ID); has {
|
||||
continue
|
||||
}
|
||||
out = p.requiredChain(s, reg, req, n, out)
|
||||
out = append(out, req)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func inScene(s *scene.Scene, t schema.TypeID) bool {
|
||||
for _, h := range s.PreOrder() {
|
||||
if _, ok := s.GetByType(h, t); ok && s.IsAlive(h) {
|
||||
@@ -781,6 +793,9 @@ func (p *Panel) addComponent(d *document.SceneDocument) {
|
||||
widgets.HeaderIcon(ui, meta.Icon, label+" — "+why, true)
|
||||
continue
|
||||
}
|
||||
for _, req := range p.requiredChain(s, reg, td, p.nodes[0], nil) {
|
||||
label += " + " + p.reg.Meta(req).DisplayName
|
||||
}
|
||||
if first == nil {
|
||||
first = td
|
||||
}
|
||||
@@ -803,18 +818,21 @@ func (p *Panel) addComponent(d *document.SceneDocument) {
|
||||
func (p *Panel) add(d *document.SceneDocument, td *schema.TypeDescriptor) {
|
||||
p.confirm(d, "Add "+p.reg.Meta(td).DisplayName, func() error {
|
||||
s := d.Scene()
|
||||
reg := s.Registry()
|
||||
env := commands.WithOnAdd(commands.EnvFor(s), p.d.Clip.OnAdd)
|
||||
defaults := p.reg.Defaults(td.ID)
|
||||
tx := d.History.Begin("Add " + td.FullName)
|
||||
for _, n := range p.nodes {
|
||||
if _, ok := s.GetByType(s.Handle(n), td.ID); ok {
|
||||
list := append(p.requiredChain(s, reg, td, n, nil), td)
|
||||
for _, t := range list {
|
||||
if _, ok := s.GetByType(s.Handle(n), t.ID); ok {
|
||||
continue
|
||||
}
|
||||
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, n, td, defaults))); err != nil {
|
||||
if err := tx.Exec(commands.NewMutation("Add "+t.FullName, env, commands.AddComp(s, n, t, p.reg.Defaults(t.ID)))); err != nil {
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -187,8 +187,18 @@ func (p *Panel) thumbRow(id core.ID, label string, asset ids.AssetID, selected b
|
||||
th := &ui.Theme
|
||||
h := th.RowHeight + 8
|
||||
r := ui.Cursor().Next(h)
|
||||
ui.FitPopup(h + ui.TextSize(label).X + 20)
|
||||
clicked := ui.Clicked(id, r)
|
||||
hit := r
|
||||
var play mathx.Rect
|
||||
playing, audible := false, false
|
||||
if p.d.Audition != nil && !asset.IsZero() {
|
||||
playing, audible = p.d.Audition(asset, false)
|
||||
}
|
||||
if audible {
|
||||
play = mathx.Rect{X: r.X + r.W - h, Y: r.Y, W: h, H: h}
|
||||
hit.W -= h
|
||||
}
|
||||
ui.FitPopup(h + ui.TextSize(label).X + 20 + r.W - hit.W)
|
||||
clicked := ui.Clicked(id, hit)
|
||||
switch {
|
||||
case selected:
|
||||
ui.Rect(r, th.WidgetDown)
|
||||
@@ -203,6 +213,15 @@ func (p *Panel) thumbRow(id core.ID, label string, asset ids.AssetID, selected b
|
||||
}
|
||||
}
|
||||
ui.Text(label, mathx.Vec2{X: ir.X + ir.W + 8, Y: ui.TextY(r)}, th.Text)
|
||||
if audible {
|
||||
icon, tip := "play.start", "Play"
|
||||
if playing {
|
||||
icon, tip = "play.stop", "Stop"
|
||||
}
|
||||
if widgets.IconButton(ui, core.IDSeq(id, 1), play, icon, tip) {
|
||||
p.d.Audition(asset, true)
|
||||
}
|
||||
}
|
||||
return clicked
|
||||
}
|
||||
|
||||
|
||||
@@ -584,6 +584,7 @@ func (a *App) Init(e *engine.Engine) error {
|
||||
Pipeline: a.opt.Pipeline,
|
||||
Thumbs: a.thumbs,
|
||||
AssetView: a.assetView,
|
||||
Audition: a.audition,
|
||||
Play: a.play,
|
||||
Code: func() context.ScriptNav { return a.ctx.Code },
|
||||
Clip: edclip.EnvOf(a.ctx),
|
||||
@@ -1194,6 +1195,36 @@ func (a *App) viewable(k asset.ArtifactKind) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (a *App) audition(id ids.AssetID, toggle bool) (bool, bool) {
|
||||
if a.opt.Pipeline == nil || len(a.ctx.AssetPlayers) == 0 {
|
||||
return false, false
|
||||
}
|
||||
e, ok := a.opt.Pipeline.Entry(id)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
key := string(e.Kind)
|
||||
if e.Kind == asset.ArtData && a.data != nil {
|
||||
if key, ok = a.data.TypeOf(id); !ok {
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
pl, ok := a.ctx.AssetPlayers[key]
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
playing := pl.Playing(a.ctx) == id
|
||||
if toggle {
|
||||
if playing {
|
||||
pl.Stop(a.ctx)
|
||||
} else if err := pl.Play(a.ctx, id); err != nil {
|
||||
a.ctx.Notify(context.LevelError, err.Error())
|
||||
}
|
||||
playing = pl.Playing(a.ctx) == id
|
||||
}
|
||||
return playing, true
|
||||
}
|
||||
|
||||
func (a *App) assetView(id ids.AssetID) bool {
|
||||
if a.opt.Pipeline == nil {
|
||||
return false
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
File diff suppressed because it is too large
Load Diff
@@ -259,6 +259,7 @@ func (p *plugin) transport(ed *context.Editor, mod *audio.Module, v ClipView) {
|
||||
return mathx.Rect{X: row.X + float32(i)*(w+4), Y: row.Y, W: w, H: row.H}
|
||||
}
|
||||
if widgets.ButtonRect(ui, ui.ID("play"), at(0), "Play") && mod != nil {
|
||||
p.aud = audition{}
|
||||
p.notify(mod.Preview(ed.Loader, v.ID, p.clip.loop))
|
||||
}
|
||||
if widgets.ButtonRect(ui, ui.ID("stop"), at(1), "Stop") && mod != nil {
|
||||
|
||||
@@ -39,6 +39,7 @@ type plugin struct {
|
||||
ed *context.Editor
|
||||
mod *audio.Module
|
||||
clip clipEdit
|
||||
aud audition
|
||||
}
|
||||
|
||||
func lookup(sr *schema.Registry, name string) (*schema.TypeDescriptor, error) {
|
||||
@@ -72,6 +73,12 @@ func Hook(e *ext.Registry) error {
|
||||
if err := e.AssetInspector(string(asset.ArtAudio), p.clipInspector); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.AssetPlayer(string(asset.ArtAudio), context.AssetPlayer{Play: p.playClip, Stop: p.stopAudition, Playing: p.clipPlaying}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.AssetPlayer(audio.NameEvent, context.AssetPlayer{Play: p.playEvent, Stop: p.stopAudition, Playing: p.eventPlaying}); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Overlay(p.sceneOverlay)
|
||||
e.GameOverlay(p.gameOverlay)
|
||||
if err := e.Panel(workspace.Desc{Panel: newMixer(p), Near: workspace.Console, Order: mixerOrder, Icon: "panel.mixer"}); err != nil {
|
||||
|
||||
@@ -47,6 +47,10 @@ type blend struct {
|
||||
t float32
|
||||
dur float32
|
||||
on bool
|
||||
// stale: from was not drawn last tick (a rewind replays without drawing); the next Adjust takes it from prev.
|
||||
stale bool
|
||||
prev scene.NodeHandle
|
||||
prevZone scene.NodeHandle
|
||||
}
|
||||
|
||||
type output struct {
|
||||
@@ -227,7 +231,7 @@ func (s *Service) reselect(o *output) {
|
||||
}
|
||||
|
||||
func (s *Service) switchTo(o *output, c cand) {
|
||||
prev := o.active
|
||||
prev, prevZone := o.active, o.zone
|
||||
s.endBlend(o, false)
|
||||
o.active, o.zone = c.vc, c.zone
|
||||
if prev.IsZero() || c.vc.IsZero() || o.forcedCut {
|
||||
@@ -243,7 +247,8 @@ func (s *Service) switchTo(o *output, c cand) {
|
||||
return
|
||||
}
|
||||
s.nextKey++
|
||||
o.blend = blend{key: s.nextKey, from: o.logical, dur: dur, on: true}
|
||||
fresh := o.hasView && o.frame+1 >= s.ticks
|
||||
o.blend = blend{key: s.nextKey, from: o.logical, dur: dur, on: true, stale: !fresh, prev: prev, prevZone: prevZone}
|
||||
}
|
||||
|
||||
func (s *Service) advance(o *output) {
|
||||
@@ -309,6 +314,12 @@ func (s *Service) Adjust(node scene.NodeHandle, v view.View) view.View {
|
||||
} else {
|
||||
s.stepFollows(o, base)
|
||||
p := s.vcPose(o.active, o.zone, base)
|
||||
if o.blend.on && o.blend.stale {
|
||||
o.blend.from, o.blend.stale = p, false
|
||||
if s.scene.IsAlive(o.blend.prev) {
|
||||
o.blend.from = s.vcPose(o.blend.prev, o.blend.prevZone, base)
|
||||
}
|
||||
}
|
||||
if o.blend.on {
|
||||
p = lerpPose(o.blend.from, p, s.blendK(o))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build !aego_runtime
|
||||
|
||||
package gameplay
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
|
||||
"aego/engine/module"
|
||||
)
|
||||
|
||||
//go:embed res/docs
|
||||
var docFiles embed.FS
|
||||
|
||||
func registerDocs(r *module.Registry) {
|
||||
if sub, err := fs.Sub(docFiles, "res/docs"); err == nil {
|
||||
r.Docs(sub)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build aego_runtime
|
||||
|
||||
package gameplay
|
||||
|
||||
import "aego/engine/module"
|
||||
|
||||
func registerDocs(*module.Registry) {}
|
||||
@@ -0,0 +1,56 @@
|
||||
package gameplay
|
||||
|
||||
import (
|
||||
"aego/core/mathx"
|
||||
"aego/core/rewind"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
var _ rewind.Keyframer = (*Service)(nil)
|
||||
|
||||
// noKey: the service keeps no state between steps; the controllers' state rides in the scene key.
|
||||
type noKey struct{}
|
||||
|
||||
func (noKey) Size() int { return 0 }
|
||||
|
||||
func (s *Service) Capture(rewind.Key) rewind.Key {
|
||||
return noKey{}
|
||||
}
|
||||
|
||||
func (s *Service) Restore(rewind.Key) {}
|
||||
|
||||
// Hash adds the unexported controller state, which the scene hash cannot see.
|
||||
func (s *Service) Hash(h *rewind.Hasher) {
|
||||
h.String(ServiceID)
|
||||
s.hh = h
|
||||
scene.Query1[Motion2D](s.scene).Each(s.hashFn)
|
||||
s.hh = nil
|
||||
}
|
||||
|
||||
func hashVec(h *rewind.Hasher, v mathx.Vec2) {
|
||||
h.F32(v.X)
|
||||
h.F32(v.Y)
|
||||
}
|
||||
|
||||
func (s *Service) hashOne(n scene.NodeHandle, c *Motion2D) {
|
||||
h, st := s.hh, &c.st
|
||||
h.U32(n.Index)
|
||||
h.U32(n.Gen)
|
||||
hashVec(h, st.move)
|
||||
hashVec(h, st.dashVel)
|
||||
hashVec(h, st.addVel)
|
||||
hashVec(h, st.teleportTo)
|
||||
hashVec(h, st.floorN)
|
||||
hashVec(h, st.platVel)
|
||||
hashVec(h, st.wallN)
|
||||
h.F32(st.dashLeft)
|
||||
h.F32(st.coyote)
|
||||
h.F32(st.buffer)
|
||||
h.F32(st.landSpeed)
|
||||
h.U32(st.ground.Index)
|
||||
h.U32(st.ground.Gen)
|
||||
h.U32(uint32(st.events))
|
||||
for _, b := range [...]bool{st.jumpReq, st.jumpRel, st.jumping, st.stop, st.teleport, st.grounded} {
|
||||
h.Bool(b)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
MODULE_CI += test-gameplay
|
||||
|
||||
.PHONY: test-gameplay bench-gameplay
|
||||
|
||||
test-gameplay:
|
||||
go test ./modules/gameplay/... ./tests/gameplay/...
|
||||
|
||||
bench-gameplay:
|
||||
$(BENCH_PIN) go test -run '^$$' -bench Motion100 -benchmem -count=$(BENCH_COUNT) ./tests/gameplay
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
---
|
||||
id: gameplay
|
||||
try: try/Motion2D.aescene
|
||||
try_select: gameplay.Motion2D
|
||||
icon: gameplay.motion
|
||||
group: gameplay
|
||||
title: Character motion
|
||||
order: 2
|
||||
---
|
||||
# Character motion
|
||||
|
||||
[Motion 2D](ref:gameplay.Motion2D) moves a character with a kinematic [Rigid Body 2D](ref:physics.Body2D). Each fixed step it casts the body's colliders along the move, slides along what it hits and moves the body to the result, so the character never sinks into walls and still pushes crates.
|
||||
|
||||
Quick start: give the player node a Rigid Body 2D with Type kinematic, a collider and Motion 2D. From a script call `Move` with the horizontal input and `Jump` when the jump button goes down:
|
||||
|
||||
```aes
|
||||
m := node.Get<Motion2D>()
|
||||
m.Move(input.Axis("Move"))
|
||||
if input.Pressed("Jump") {
|
||||
m.Jump()
|
||||
}
|
||||
if input.Released("Jump") {
|
||||
m.JumpReleased()
|
||||
}
|
||||
```
|
||||
|
||||
`Move` stays until the next call; `Jump` is one request that the controller keeps for Jump Buffer seconds, so a jump pressed just before landing still happens.
|
||||
|
||||
| Mode | Behaviour |
|
||||
|---|---|
|
||||
| platformer | walks on floors under gravity, jumps, climbs steps and slopes |
|
||||
| topDown | moves freely in the plane with `MoveDir`, slides along walls |
|
||||
|
||||
Floors, walls and ceilings are told apart by the angle of the surface to Up Direction: a surface steeper than Max Slope is a wall. On a floor the character follows its slope without sliding down, and Snap To Ground keeps it on the floor when walking down a slope or a small step. Steps up to Step Height are climbed without jumping.
|
||||
|
||||
The character rides on the body it stands on when Moving Platforms is on; Platform On Leave decides what happens to the platform's speed after a jump. With Push Bodies on, dynamic bodies do not stop the character, it pushes them.
|
||||
|
||||
Coyote Time lets a jump work a moment after walking off a ledge, and Jump Cut Multiplier shortens a jump when the button is released early. `Dash`, `AddVelocity`, `Stop` and `Teleport` cover dashes, knockback and respawns.
|
||||
|
||||
Signals `Landed`, `LeftGround`, `Jumped`, `HitWall` and `HitCeiling` arrive in the same fixed step as collisions. The Runtime fields show the velocity, whether the character is grounded or touching a wall, and the floor it stands on.
|
||||
|
||||
**Problems** lists a platformer with Gravity Scale 0 and no Jump Speed (GMP002). A Motion 2D on a body that is not kinematic cannot move it; the console says so once (GMP001).
|
||||
@@ -0,0 +1,49 @@
|
||||
package demo
|
||||
|
||||
import "aego/input"
|
||||
|
||||
// Player walks with A/D or the arrows and jumps with Space, W or Up.
|
||||
script Player {
|
||||
func Update(dt float) {
|
||||
m := node.Get<Motion2D>()
|
||||
x := 0.0
|
||||
if input.Down(input.KeyA) || input.Down(input.KeyLeft) {
|
||||
x -= 1
|
||||
}
|
||||
if input.Down(input.KeyD) || input.Down(input.KeyRight) {
|
||||
x += 1
|
||||
}
|
||||
m.Move(x)
|
||||
if input.KeyPressed(input.KeySpace) || input.KeyPressed(input.KeyW) || input.KeyPressed(input.KeyUp) {
|
||||
m.Jump()
|
||||
}
|
||||
if input.KeyReleased(input.KeySpace) || input.KeyReleased(input.KeyW) || input.KeyReleased(input.KeyUp) {
|
||||
m.JumpReleased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lift moves a kinematic platform up and down.
|
||||
script Lift {
|
||||
@export var Travel Vec2 = Vec2{0, -3}
|
||||
@export var Period float = 4
|
||||
|
||||
var t float
|
||||
var start Vec2
|
||||
|
||||
func Ready() {
|
||||
start = node.Transform2D().Position
|
||||
}
|
||||
|
||||
func FixedUpdate(dt float) {
|
||||
t += dt
|
||||
phase := t / Period
|
||||
k := phase - float(int(phase))
|
||||
if k > 0.5 {
|
||||
k = 1 - k
|
||||
}
|
||||
k *= 2
|
||||
body := node.Get<Body2D>()
|
||||
body.MoveTo(Vec2{start.X + Travel.X*k, start.Y + Travel.Y*k}, 0)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M4.5 4L11.5 4L15 11.5L13 12.5L10.6 9.8L5.4 9.8L3 12.5L1 11.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 152 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#fff" d="M9 3L14.5 8L9 13L9 10L5 10L5 6L9 6Z"/><path fill="#fff" d="M1 6L3.6 6L3.6 7.3L1 7.3Z"/><path fill="#fff" d="M1 8.7L3.6 8.7L3.6 10L1 10Z"/></svg>
|
||||
|
After Width: | Height: | Size: 226 B |
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/modules/physics/components"
|
||||
"aego/modules/physics/world"
|
||||
@@ -199,7 +197,7 @@ func (m *Sim) Present(alpha float32) {
|
||||
func (m *Sim) Teleport(h scene.NodeHandle, p mathx.Vec2, a float32) error {
|
||||
b := m.byBody[h]
|
||||
if b == nil || b.h.IsZero() {
|
||||
return errors.New("node has no simulated physics body")
|
||||
return errNoBody
|
||||
}
|
||||
if err := m.w.SetBodyTransform(b.h, p, a); err != nil {
|
||||
return err
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,13 @@ func (m *Sim) ShapeNode(h world.ShapeHandle) scene.NodeHandle {
|
||||
}
|
||||
|
||||
func (m *Sim) filter(f physics2d.QueryFilter) world.QueryFilter {
|
||||
wf := world.QueryFilter{LayerMask: f.LayerMask, IncludeTriggers: f.IncludeTriggers}
|
||||
if !f.ExcludeNode.IsZero() {
|
||||
if b := m.bodyAt(f.ExcludeNode); b != nil {
|
||||
wf := world.QueryFilter{LayerMask: f.LayerMask, IncludeTriggers: f.IncludeTriggers, SkipDynamic: f.SkipDynamic}
|
||||
ex := f.ExcludeBody
|
||||
if ex.IsZero() {
|
||||
ex = f.ExcludeNode
|
||||
}
|
||||
if !ex.IsZero() {
|
||||
if b := m.bodyAt(ex); b != nil {
|
||||
wf.ExcludeBody = b.h
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ type Sim struct {
|
||||
layers [32]string
|
||||
qhits []world.RayHit
|
||||
qshapes []world.ShapeHandle
|
||||
qcasts []world.CastHit
|
||||
}
|
||||
|
||||
// Touch is a shape pair in contact; Fresh marks pairs that began this step.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,11 +16,21 @@ type queryState struct {
|
||||
probeXf xform
|
||||
pick ShapeHandle
|
||||
picked bool
|
||||
castC convex
|
||||
castA proxy
|
||||
castCentroid Vec2
|
||||
castDelta Vec2
|
||||
castBest CastHit
|
||||
castFound bool
|
||||
penOut []CastHit
|
||||
penN int
|
||||
rayFn func(int32, Vec2, Vec2, float32) float32
|
||||
allFn func(int32, Vec2, Vec2, float32) float32
|
||||
pointFn func(int32) bool
|
||||
shapeFn func(int32) bool
|
||||
atFn func(int32) bool
|
||||
castFn func(int32) bool
|
||||
penFn func(int32) bool
|
||||
}
|
||||
|
||||
func (q *queryState) init(w *World) {
|
||||
@@ -30,6 +40,8 @@ func (q *queryState) init(w *World) {
|
||||
q.pointFn = q.pointHit
|
||||
q.shapeFn = q.shapeHit
|
||||
q.atFn = q.atHit
|
||||
q.castFn = q.castHit
|
||||
q.penFn = q.penHit
|
||||
}
|
||||
|
||||
func (q *queryState) accept(s *shape) bool {
|
||||
@@ -43,7 +55,11 @@ func (q *queryState) accept(s *shape) bool {
|
||||
if !f.ExcludeBody.IsZero() && s.body == f.ExcludeBody {
|
||||
return false
|
||||
}
|
||||
return (f.LayerMask>>q.w.bodies.items[s.body.Index].layer)&1 != 0
|
||||
b := &q.w.bodies.items[s.body.Index]
|
||||
if f.SkipDynamic && b.typ == Dynamic {
|
||||
return false
|
||||
}
|
||||
return (f.LayerMask>>b.layer)&1 != 0
|
||||
}
|
||||
|
||||
func (q *queryState) cast(s *shape, child int, maxT float32) (float32, Vec2, bool) {
|
||||
|
||||
@@ -136,6 +136,7 @@ type Events struct {
|
||||
type QueryFilter struct {
|
||||
LayerMask uint32
|
||||
IncludeTriggers bool
|
||||
SkipDynamic bool
|
||||
ExcludeBody BodyHandle
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -182,6 +182,14 @@
|
||||
"owner": "camera",
|
||||
"doc": "Picks which output of virtual cameras drives this Camera 2D. Without it the camera shows the main output."
|
||||
},
|
||||
{
|
||||
"name": "gameplay.Motion2D",
|
||||
"kind": "component",
|
||||
"display": "Motion 2D",
|
||||
"category": "Gameplay",
|
||||
"owner": "gameplay",
|
||||
"doc": "Moves a kinematic Body2D by casting its colliders: walking, jumping, slopes, steps and moving platforms, or free top-down movement. Call Move and Jump from a script; the controller acts on the next fixed step."
|
||||
},
|
||||
{
|
||||
"name": "save.Persist",
|
||||
"kind": "component",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/gpu"
|
||||
"aego/modules/camera"
|
||||
"aego/render/rt"
|
||||
)
|
||||
|
||||
// A zone blend that starts while a seek replays undrawn ticks must not blend from a zero or stale pose.
|
||||
func TestCameraRewindKeepsZoom(t *testing.T) {
|
||||
for _, every := range []uint64{60, 1 << 30} {
|
||||
s, pc := rewindSession(t)
|
||||
pc.SetKeyEvery(every)
|
||||
e := s.Host.Engine
|
||||
pool := rt.New(e.GPU(), gpu.TexSRGBA8, "test.camera")
|
||||
tgt, err := pool.Resize(nil, 320, 180, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
render := func() float32 {
|
||||
frame := e.GPU().BeginFrame(320, 180)
|
||||
if err := s.Renderer.BeginFrame(frame, 320, 180, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc.Render(tgt)
|
||||
s.Renderer.EndFrame()
|
||||
e.GPU().EndFrame()
|
||||
return camera.Of(pc.World()).Render("").Zoom
|
||||
}
|
||||
zoom := map[int]float32{}
|
||||
lo, hi := float32(1e9), float32(0)
|
||||
drive(e, pc, 0, 800, func(f int) {
|
||||
z := render()
|
||||
zoom[f] = z
|
||||
lo, hi = min(lo, z), max(hi, z)
|
||||
})
|
||||
for n := 790; n > 0; n -= 13 {
|
||||
if err := pc.Seek(n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
z := render()
|
||||
if z < lo-0.01 || z > hi+0.01 {
|
||||
t.Fatalf("keys every %d, seek %d: zoom %v outside the run's %v..%v", every, n, z, lo, hi)
|
||||
}
|
||||
// Restoring a keyframe finishes unscaled blends (D306); a replay from the start keeps them.
|
||||
if every > 1000 && (z < zoom[n]-0.01 || z > zoom[n]+0.01) {
|
||||
t.Fatalf("replayed seek %d: zoom %v, was %v", n, z, zoom[n])
|
||||
}
|
||||
}
|
||||
pool.Release(tgt)
|
||||
pool.Close()
|
||||
}
|
||||
}
|
||||
+233
-1
File diff suppressed because it is too large
Load Diff
+1
@@ -20,4 +20,5 @@ Physics physics.RevoluteJoint2D Revolute Joint 2D comp.joint_revolute -
|
||||
Audio audio.AudioListener2D Audio Listener 2D audio.listener -
|
||||
Audio audio.AudioSource2D Audio Source 2D audio.source -
|
||||
Audio audio.AudioZone2D Audio Zone 2D audio.zone -
|
||||
Gameplay gameplay.Motion2D Motion 2D gameplay.motion -
|
||||
game game.Rotator Rotator cat.game rotator
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user