273 lines
12 KiB
Go
273 lines
12 KiB
Go
package gameplay
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"aego/core/mathx"
|
|
"aego/engine/module"
|
|
"aego/engine/problems"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
"aego/script/bind"
|
|
)
|
|
|
|
const (
|
|
NameMotion = "gameplay.Motion2D"
|
|
bodyName = "physics.Body2D"
|
|
layersList = "layers"
|
|
|
|
CodeNotKinematic = "GMP001"
|
|
CodeNoJump = "GMP002"
|
|
)
|
|
|
|
type Mode uint8
|
|
|
|
const (
|
|
Platformer Mode = iota
|
|
TopDown
|
|
)
|
|
|
|
type LeaveMode uint8
|
|
|
|
const (
|
|
LeaveKeep LeaveMode = iota
|
|
LeaveDrop
|
|
LeaveAdd
|
|
)
|
|
|
|
const (
|
|
evLanded uint8 = 1 << iota
|
|
evLeft
|
|
evJumped
|
|
evWall
|
|
evCeiling
|
|
)
|
|
|
|
type motionState struct {
|
|
move mathx.Vec2
|
|
jumpReq bool
|
|
jumpRel bool
|
|
jumping bool
|
|
dashVel mathx.Vec2
|
|
dashLeft float32
|
|
addVel mathx.Vec2
|
|
stop bool
|
|
teleport bool
|
|
teleportTo mathx.Vec2
|
|
coyote float32
|
|
buffer float32
|
|
grounded bool
|
|
floorN mathx.Vec2
|
|
ground scene.NodeHandle
|
|
platVel mathx.Vec2
|
|
events uint8
|
|
landSpeed float32
|
|
wallN mathx.Vec2
|
|
warned bool
|
|
}
|
|
|
|
type Motion2D struct {
|
|
Mode Mode `aego:"mode"`
|
|
UpDirection mathx.Vec2 `aego:"upDirection"`
|
|
MaxSpeed float32 `aego:"maxSpeed,min=0"`
|
|
Accel float32 `aego:"accel,min=0"`
|
|
Decel float32 `aego:"decel,min=0"`
|
|
AirControl float32 `aego:"airControl,min=0,max=1"`
|
|
GravityScale float32 `aego:"gravityScale"`
|
|
MaxFallSpeed float32 `aego:"maxFallSpeed,min=0"`
|
|
JumpHeight float32 `aego:"jumpHeight,min=0"`
|
|
JumpSpeed float32 `aego:"jumpSpeed,min=0"`
|
|
JumpCut float32 `aego:"jumpCutMultiplier,min=0,max=1"`
|
|
CoyoteTime float32 `aego:"coyoteTime,min=0"`
|
|
JumpBuffer float32 `aego:"jumpBuffer,min=0"`
|
|
MaxSlope float32 `aego:"maxSlope,min=0,max=89"`
|
|
StepHeight float32 `aego:"stepHeight,min=0"`
|
|
SnapToGround float32 `aego:"snapToGround,min=0"`
|
|
SafeMargin float32 `aego:"safeMargin,min=0.001"`
|
|
MaxSlides int32 `aego:"maxSlides,min=1,max=8"`
|
|
CollisionMask int64 `aego:"collisionMask"`
|
|
PushBodies bool `aego:"pushBodies"`
|
|
MovingPlatforms bool `aego:"movingPlatforms"`
|
|
PlatformOnLeave LeaveMode `aego:"platformOnLeave"`
|
|
FaceMovement bool `aego:"faceMovement"`
|
|
DashGravity bool `aego:"dashGravity"`
|
|
Velocity mathx.Vec2 `aego:"velocity,transient,readonly,category=Runtime"`
|
|
Grounded bool `aego:"grounded,transient,readonly,category=Runtime"`
|
|
OnWall bool `aego:"onWall,transient,readonly,category=Runtime"`
|
|
OnCeiling bool `aego:"onCeiling,transient,readonly,category=Runtime"`
|
|
WallNormal mathx.Vec2 `aego:"wallNormal,transient,readonly,category=Runtime"`
|
|
FloorNormal mathx.Vec2 `aego:"floorNormal,transient,readonly,category=Runtime"`
|
|
FloorAngle float32 `aego:"floorAngle,transient,readonly,category=Runtime"`
|
|
GroundBody scene.NodeRef `aego:"groundBody,transient,readonly,category=Runtime"`
|
|
|
|
st motionState
|
|
}
|
|
|
|
func DefaultMotion() Motion2D {
|
|
return Motion2D{
|
|
UpDirection: mathx.Vec2{Y: -1}, MaxSpeed: 6, Accel: 60, Decel: 50, AirControl: 0.6,
|
|
GravityScale: 1, MaxFallSpeed: 20, JumpHeight: 2.2, JumpCut: 0.5, CoyoteTime: 0.1, JumpBuffer: 0.1,
|
|
MaxSlope: 45, StepHeight: 0.2, SnapToGround: 0.2, SafeMargin: 0.02, MaxSlides: 4, CollisionMask: -1,
|
|
PushBodies: true, MovingPlatforms: true,
|
|
}
|
|
}
|
|
|
|
func (m *Motion2D) Move(x float32) {
|
|
m.st.move = mathx.Vec2{X: mathx.Clamp(x, -1, 1)}
|
|
}
|
|
|
|
func (m *Motion2D) MoveDir(v mathx.Vec2) {
|
|
m.st.move = v
|
|
}
|
|
|
|
func (m *Motion2D) Jump() {
|
|
m.st.jumpReq = true
|
|
}
|
|
|
|
func (m *Motion2D) JumpReleased() {
|
|
m.st.jumpRel = true
|
|
}
|
|
|
|
func (m *Motion2D) Dash(v mathx.Vec2, seconds float32) {
|
|
m.st.dashVel, m.st.dashLeft = v, max(seconds, 0)
|
|
}
|
|
|
|
func (m *Motion2D) AddVelocity(v mathx.Vec2) {
|
|
m.st.addVel = m.st.addVel.Add(v)
|
|
}
|
|
|
|
func (m *Motion2D) Stop() {
|
|
m.st.stop = true
|
|
}
|
|
|
|
func (m *Motion2D) Teleport(p mathx.Vec2) {
|
|
m.st.teleport, m.st.teleportTo = true, p
|
|
}
|
|
|
|
const kinematic = 1
|
|
|
|
type signals struct {
|
|
landed module.Signal1[float32]
|
|
left module.Signal0
|
|
jumped module.Signal0
|
|
wall module.Signal1[mathx.Vec2]
|
|
ceiling module.Signal0
|
|
damaged module.Signal2[int64, scene.NodeHandle]
|
|
healed module.Signal1[int64]
|
|
died module.Signal1[scene.NodeHandle]
|
|
revived module.Signal0
|
|
hit module.Signal2[scene.NodeHandle, int64]
|
|
reached module.Signal1[scene.NodeHandle]
|
|
respawned module.Signal0
|
|
collected module.Signal1[scene.NodeHandle]
|
|
changed module.Signal2[int64, int64]
|
|
}
|
|
|
|
func bodyType(s *scene.Scene) (*schema.TypeDescriptor, schema.FieldID, bool) {
|
|
td, ok := s.Registry().ByName(bodyName)
|
|
if !ok {
|
|
return nil, 0, false
|
|
}
|
|
fd, ok := td.Field("type")
|
|
if !ok {
|
|
return nil, 0, false
|
|
}
|
|
return td, fd.ID, true
|
|
}
|
|
|
|
func validateMotion(ctx module.ValidateContext, m *Motion2D) []problems.Diagnostic {
|
|
var out []problems.Diagnostic
|
|
if td, f, ok := bodyType(ctx.Scene); ok {
|
|
if v, err := ctx.Scene.GetProperty(ctx.Node, td.ID, f); err == nil && v.I != kinematic {
|
|
out = append(out, problems.Diagnostic{Code: CodeNotKinematic, Severity: problems.Error, BlocksPlay: true,
|
|
Message: "Motion 2D moves its body itself: set Body 2D Type to kinematic"})
|
|
}
|
|
}
|
|
if m.Mode == Platformer && m.GravityScale == 0 && m.JumpSpeed == 0 && m.JumpHeight > 0 {
|
|
out = append(out, problems.Diagnostic{Code: CodeNoJump, Severity: problems.Error,
|
|
Message: "Motion 2D: with Gravity Scale 0 the jump speed cannot come from Jump Height, set Jump Speed"})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func kinematicOnAdd(ctx module.AddContext, _ *Motion2D) {
|
|
if td, f, ok := bodyType(ctx.Scene); ok {
|
|
_ = ctx.Scene.SetProperty(ctx.Node, td.ID, f, schema.Enum(kinematic))
|
|
}
|
|
}
|
|
|
|
func registerMotion(r *module.Registry, sig *signals) error {
|
|
if err := errors.Join(
|
|
module.Enum[Mode](r, "gameplay.MotionMode",
|
|
schema.EnumValue{Name: "platformer", Value: int64(Platformer)},
|
|
schema.EnumValue{Name: "topDown", Value: int64(TopDown)}),
|
|
module.Enum[LeaveMode](r, "gameplay.PlatformLeave",
|
|
schema.EnumValue{Name: "keep", Value: int64(LeaveKeep)},
|
|
schema.EnumValue{Name: "drop", Value: int64(LeaveDrop)},
|
|
schema.EnumValue{Name: "add", Value: int64(LeaveAdd)}),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
b := module.Component[Motion2D](r, schema.Name(NameMotion), schema.Space(schema.Space2D),
|
|
schema.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."),
|
|
schema.FieldDoc("mode", "platformer walks on floors under gravity; topDown moves freely in the plane."),
|
|
schema.FieldDoc("upDirection", "Up for floors, jumps and gravity. (0, -1) is up on screen."),
|
|
schema.FieldDoc("maxSpeed", "Top speed in units per second."),
|
|
schema.FieldDoc("accel", "How fast the speed grows towards the target, units per second squared."),
|
|
schema.FieldDoc("decel", "How fast the speed falls without input."),
|
|
schema.FieldDoc("airControl", "Share of Accel and Decel available in the air."),
|
|
schema.FieldDoc("gravityScale", "Multiplies the physics gravity."),
|
|
schema.FieldDoc("maxFallSpeed", "Fastest fall in units per second. 0 is unlimited."),
|
|
schema.FieldDoc("jumpHeight", "Height of a full jump; sets the jump speed unless Jump Speed is set."),
|
|
schema.FieldDoc("jumpSpeed", "Upward speed of a jump. 0 derives it from Jump Height and gravity."),
|
|
schema.FieldDoc("jumpCutMultiplier", "Upward speed is multiplied by this when the jump button is released early."),
|
|
schema.FieldDoc("coyoteTime", "Seconds after leaving a ledge during which a jump still works."),
|
|
schema.FieldDoc("jumpBuffer", "Seconds a jump pressed in the air is remembered until landing."),
|
|
schema.FieldDoc("maxSlope", "Steepest floor in degrees; steeper surfaces are walls."),
|
|
schema.FieldDoc("stepHeight", "Steps up to this height are climbed without jumping."),
|
|
schema.FieldDoc("snapToGround", "Keeps the character on slopes and steps down by up to this distance."),
|
|
schema.FieldDoc("safeMargin", "Gap kept to surfaces so casts never start inside them."),
|
|
schema.FieldDoc("maxSlides", "How many surfaces one step may slide along."),
|
|
schema.FieldDoc("collisionMask", "Layers the character collides with."),
|
|
schema.FieldDoc("pushBodies", "Moves through dynamic bodies and pushes them instead of stopping at them."),
|
|
schema.FieldDoc("movingPlatforms", "Carries the character with the body it stands on."),
|
|
schema.FieldDoc("platformOnLeave", "keep keeps the platform's velocity after leaving it, drop loses it, add keeps only its horizontal part."),
|
|
schema.FieldDoc("faceMovement", "topDown: turns the node towards the movement."),
|
|
schema.FieldDoc("dashGravity", "Applies gravity during a dash."),
|
|
schema.FieldDoc("velocity", "Current velocity in units per second, during Play."),
|
|
schema.FieldDoc("grounded", "Whether the character stands on a floor, during Play."),
|
|
schema.FieldDoc("onWall", "Whether the character touched a wall in the last step, during Play."),
|
|
schema.FieldDoc("onCeiling", "Whether the character touched a ceiling in the last step, during Play."),
|
|
schema.FieldDoc("wallNormal", "Normal of the last wall touched, during Play."),
|
|
schema.FieldDoc("floorNormal", "Normal of the floor under the character, during Play."),
|
|
schema.FieldDoc("floorAngle", "Slope of the floor in degrees, during Play."),
|
|
schema.FieldDoc("groundBody", "Body the character stands on, during Play."),
|
|
schema.Runtime("velocity", "grounded", "onWall", "onCeiling", "wallNormal", "floorNormal", "floorAngle", "groundBody"),
|
|
schema.Bitset("collisionMask", needsPhysics, layersList),
|
|
).Requires(bodyName).Category(Category).Display("Motion 2D").Icon("gameplay.motion").NodeIcon(75).Default(DefaultMotion).Validate(validateMotion).OnAdd(kinematicOnAdd)
|
|
|
|
b.Proc1("Move", func(_ *bind.Ctx, m *Motion2D, x float32) { m.Move(x) },
|
|
bind.Doc("Move sets the horizontal input, -1..1; it stays until the next Move."), bind.Params("x"))
|
|
b.Proc1("MoveDir", func(_ *bind.Ctx, m *Motion2D, v mathx.Vec2) { m.MoveDir(v) },
|
|
bind.Doc("MoveDir sets the top-down input; a longer vector is cut to length 1."), bind.Params("dir"))
|
|
b.Proc0("Jump", func(_ *bind.Ctx, m *Motion2D) { m.Jump() },
|
|
bind.Doc("Jump jumps on the next step if grounded, within coyote time or later within the jump buffer."))
|
|
b.Proc0("JumpReleased", func(_ *bind.Ctx, m *Motion2D) { m.JumpReleased() },
|
|
bind.Doc("JumpReleased cuts a rising jump by Jump Cut Multiplier."))
|
|
b.Proc2("Dash", func(_ *bind.Ctx, m *Motion2D, v mathx.Vec2, s float32) { m.Dash(v, s) },
|
|
bind.Doc("Dash moves at a fixed velocity for some seconds."), bind.Params("velocity", "seconds"))
|
|
b.Proc1("AddVelocity", func(_ *bind.Ctx, m *Motion2D, v mathx.Vec2) { m.AddVelocity(v) },
|
|
bind.Doc("AddVelocity adds a push once, for knockback."), bind.Params("velocity"))
|
|
b.Proc0("Stop", func(_ *bind.Ctx, m *Motion2D) { m.Stop() },
|
|
bind.Doc("Stop zeroes the velocity and ends a dash."))
|
|
b.Proc1("Teleport", func(_ *bind.Ctx, m *Motion2D, p mathx.Vec2) { m.Teleport(p) },
|
|
bind.Doc("Teleport moves to a position on the next step and resets velocity and state."), bind.Params("position"))
|
|
|
|
sig.landed = b.Signal1[float32]("Landed", "Landed fires when the character touches a floor, with the falling speed.", "speed")
|
|
sig.left = b.Signal0("LeftGround", "LeftGround fires when the character walks off a floor without jumping.")
|
|
sig.jumped = b.Signal0("Jumped", "Jumped fires when a jump starts.")
|
|
sig.wall = b.Signal1[mathx.Vec2]("HitWall", "HitWall fires when the character starts touching a wall.", "normal")
|
|
sig.ceiling = b.Signal0("HitCeiling", "HitCeiling fires when the character bumps into a ceiling.")
|
|
return b.Err()
|
|
}
|