Swimming Physics and Bubble Pattern
Water pixels finally do something other than turn your character blue! * When the player character is "wet" (touching water pixels, and so appearing in a blue mask), water physics apply: gravity is slower, your jump height is halved, but you get infinite jumps to swim higher in the water. * Holding the jump key under water will incur a short delay between jumps, so that you don't just fly straight up to the surface. Tap the jump button to move up quicker, you can spam it all you want. Azulians are also able to handle being under water: * They'll sink to the bottom and keep walking back and forth normally. * If you are above them and noticed, they'll jump (swim) up towards you, aware of the water and it jumps like you do. * The Blue Azulian has the poorest vertical aggro range so it isn't a very good swimmer. The White Azulian is very good at navigating water as it can pursue the player from the furthest distance of them all. Changes to the editor: * New brush pattern added: bubbles.png * It's the default pattern now for the "water" color of all of the built-in palettes instead of ink.png * A repeating pattern of bubbles carved out showing the level wallpaper. * The old "Bubbles (circles.png)" is renamed "Circles" * The last scroll position is saved with the Level file, so when you reload the level later it's scrolled at where you left it.
This commit is contained in:
parent
4efa8d00fc
commit
94d0da78e7
BIN
assets/pattern/bubbles.png
Normal file
BIN
assets/pattern/bubbles.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
|
@ -2,9 +2,11 @@
|
|||
|
||||
const color = Self.GetTag("color");
|
||||
var playerSpeed = color === 'blue' ? 2 : 4,
|
||||
swimSpeed = playerSpeed * 0.4,
|
||||
aggroX = 250, // X/Y distance sensitivity from player
|
||||
aggroY = color === 'blue' ? 100 : 200,
|
||||
jumpSpeed = color === 'blue' ? 14 : 18,
|
||||
swimJumpSpeed = jumpSpeed * 0.4,
|
||||
animating = false,
|
||||
direction = "right",
|
||||
lastDirection = "right";
|
||||
|
@ -14,7 +16,9 @@ if (color === 'white') {
|
|||
aggroX = 1000;
|
||||
aggroY = 400;
|
||||
playerSpeed = 8;
|
||||
swimSpeed = playerSpeed * 0.4;
|
||||
jumpSpeed = 20;
|
||||
swimJumpSpeed = jumpSpeed * 0.4;
|
||||
}
|
||||
|
||||
function setupAnimations(color) {
|
||||
|
@ -30,6 +34,9 @@ function setupAnimations(color) {
|
|||
function main() {
|
||||
playerSpeed = color === 'blue' ? 2 : 4;
|
||||
|
||||
let swimJumpCooldownTick = 0, // minimum Game Tick before we can jump while swimming
|
||||
swimJumpCooldown = 10; // CONFIG: delta of ticks between jumps while swimming
|
||||
|
||||
Self.SetMobile(true);
|
||||
Self.SetGravity(true);
|
||||
Self.SetInventory(true);
|
||||
|
@ -95,8 +102,25 @@ function main() {
|
|||
sampleTick++;
|
||||
}
|
||||
|
||||
let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)),
|
||||
Vy = jump && Self.Grounded() ? parseFloat(-jumpSpeed) : Self.GetVelocity().Y;
|
||||
// Handle being underwater.
|
||||
let canJump = Self.Grounded();
|
||||
if (Self.IsWet()) {
|
||||
let tick = GetTick();
|
||||
if (tick > swimJumpCooldownTick) {
|
||||
canJump = true;
|
||||
swimJumpCooldownTick = tick + swimJumpCooldown;
|
||||
}
|
||||
}
|
||||
|
||||
// How speedy would our movement and jump be?
|
||||
let xspeed = playerSpeed, yspeed = jumpSpeed;
|
||||
if (Self.IsWet()) {
|
||||
xspeed = swimSpeed;
|
||||
yspeed = swimJumpSpeed;
|
||||
}
|
||||
|
||||
let Vx = parseFloat(xspeed * (direction === "left" ? -1 : 1)),
|
||||
Vy = jump && canJump ? parseFloat(-yspeed) : Self.GetVelocity().Y;
|
||||
Self.SetVelocity(Vector(Vx, Vy));
|
||||
|
||||
// If we changed directions, stop animating now so we can
|
||||
|
|
|
@ -41,6 +41,9 @@ var (
|
|||
PlayerAcceleration float64 = 0.12
|
||||
Gravity float64 = 7
|
||||
GravityAcceleration float64 = 0.1
|
||||
SwimGravity float64 = 3
|
||||
SwimJumpVelocity float64 = -12
|
||||
SwimJumpCooldown uint64 = 24 // number of frames of cooldown between swim-jumps
|
||||
SlopeMaxHeight = 8 // max pixel height for player to walk up a slope
|
||||
|
||||
// Default chunk size for canvases.
|
||||
|
|
|
@ -35,6 +35,7 @@ func (c *Collide) Reset() {
|
|||
c.Left = false
|
||||
c.Right = false
|
||||
c.Bottom = false
|
||||
c.InWater = false
|
||||
}
|
||||
|
||||
// Side of the collision box (top, bottom, left, right)
|
||||
|
@ -85,8 +86,6 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
|
|||
if result.Bottom {
|
||||
if !d.Grounded() {
|
||||
d.SetGrounded(true)
|
||||
} else {
|
||||
// result.Bottom = false
|
||||
}
|
||||
} else {
|
||||
d.SetGrounded(false)
|
||||
|
@ -250,10 +249,10 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
|
|||
side Side
|
||||
}
|
||||
jobs := []jobSide{ // We'll scan each side of the bounding box in parallel
|
||||
jobSide{col.Top[0], col.Top[1], Top},
|
||||
jobSide{col.Bottom[0], col.Bottom[1], Bottom},
|
||||
jobSide{col.Left[0], col.Left[1], Left},
|
||||
jobSide{col.Right[0], col.Right[1], Right},
|
||||
{col.Top[0], col.Top[1], Top},
|
||||
{col.Bottom[0], col.Bottom[1], Bottom},
|
||||
{col.Left[0], col.Left[1], Left},
|
||||
{col.Right[0], col.Right[1], Right},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
|
|
@ -225,7 +225,11 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
|
|||
// Scroll the level to the remembered position from when we went
|
||||
// to Play Mode and back. If no remembered position, this is zero
|
||||
// anyway.
|
||||
if s.RememberScrollPosition.IsZero() && s.Level != nil {
|
||||
s.UI.Canvas.ScrollTo(s.Level.ScrollPosition)
|
||||
} else {
|
||||
s.UI.Canvas.ScrollTo(s.RememberScrollPosition)
|
||||
}
|
||||
|
||||
d.Flash("Editor Mode.")
|
||||
if s.DrawingType == enum.LevelDrawing {
|
||||
|
@ -514,6 +518,9 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
|||
m.Palette = s.UI.Canvas.Palette
|
||||
m.Chunker = s.UI.Canvas.Chunker()
|
||||
|
||||
// Store the scroll position.
|
||||
m.ScrollPosition = s.UI.Canvas.Scroll
|
||||
|
||||
// Clear the modified flag on the level.
|
||||
s.UI.Canvas.SetModified(false)
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ var (
|
|||
Name: "water",
|
||||
Color: render.MustHexColor("#09F"),
|
||||
Water: true,
|
||||
Pattern: "ink.png",
|
||||
Pattern: "bubbles.png",
|
||||
},
|
||||
{
|
||||
Name: "hint",
|
||||
|
@ -89,7 +89,7 @@ var (
|
|||
Name: "water",
|
||||
Color: render.RGBA(0, 153, 255, 255),
|
||||
Water: true,
|
||||
Pattern: "ink.png",
|
||||
Pattern: "bubbles.png",
|
||||
},
|
||||
{
|
||||
Name: "hint",
|
||||
|
@ -126,7 +126,7 @@ var (
|
|||
{
|
||||
Name: "water",
|
||||
Color: render.MustHexColor("#09F"),
|
||||
Pattern: "ink.png",
|
||||
Pattern: "bubbles.png",
|
||||
},
|
||||
{
|
||||
Name: "hint",
|
||||
|
@ -159,7 +159,7 @@ var (
|
|||
Name: "water",
|
||||
Color: render.RGBA(0, 153, 255, 255),
|
||||
Water: true,
|
||||
Pattern: "ink.png",
|
||||
Pattern: "bubbles.png",
|
||||
},
|
||||
{
|
||||
Name: "electric",
|
||||
|
|
|
@ -57,6 +57,9 @@ type Level struct {
|
|||
MaxHeight int64 `json:"boundedHeight"`
|
||||
Wallpaper string `json:"wallpaper"`
|
||||
|
||||
// The last scrolled position in the editor.
|
||||
ScrollPosition render.Point `json:"scroll"`
|
||||
|
||||
// Actors keep a list of the doodad instances in this map.
|
||||
Actors ActorMap `json:"actors"`
|
||||
|
||||
|
|
|
@ -42,6 +42,10 @@ var Builtins = []Pattern{
|
|||
},
|
||||
{
|
||||
Name: "Bubbles",
|
||||
Filename: "bubbles.png",
|
||||
},
|
||||
{
|
||||
Name: "Circles",
|
||||
Filename: "circles.png",
|
||||
},
|
||||
{
|
||||
|
|
|
@ -74,6 +74,7 @@ type PlayScene struct {
|
|||
godMode bool // Cheat: player can't die
|
||||
godModeUntil time.Time // Invulnerability timer at respawn.
|
||||
playerJumpCounter int // limit jump length
|
||||
jumpCooldownUntil uint64 // future game tick for jump cooldown (swimming esp.)
|
||||
|
||||
// Inventory HUD. Impl. in play_inventory.go
|
||||
invenFrame *ui.Frame
|
||||
|
@ -211,6 +212,8 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
|
|||
|
||||
// Handler when an actor touches water or fire.
|
||||
s.drawing.OnLevelCollision = func(a *uix.Actor, col *collision.Collide) {
|
||||
a.SetWet(col.InWater)
|
||||
|
||||
if col.InFire != "" {
|
||||
a.Canvas.MaskColor = render.Black
|
||||
if a.ID() == "PLAYER" { // only the player dies in fire.
|
||||
|
@ -473,7 +476,6 @@ func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, cen
|
|||
// Set up the movement physics for the player.
|
||||
s.playerPhysics = &physics.Mover{
|
||||
MaxSpeed: physics.NewVector(balance.PlayerMaxVelocity, balance.PlayerMaxVelocity),
|
||||
// Gravity: physics.NewVector(balance.Gravity, balance.Gravity),
|
||||
Acceleration: 0.025,
|
||||
Friction: 0.1,
|
||||
}
|
||||
|
@ -804,111 +806,6 @@ func (s *PlayScene) Draw(d *Doodle) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// movePlayer updates the player's X,Y coordinate based on key pressed.
|
||||
func (s *PlayScene) movePlayer(ev *event.State) {
|
||||
var (
|
||||
playerSpeed = float64(balance.PlayerMaxVelocity)
|
||||
velocity = s.Player.Velocity()
|
||||
direction float64
|
||||
jumping bool
|
||||
)
|
||||
|
||||
// Antigravity: player can move anywhere with arrow keys.
|
||||
if s.antigravity || !s.Player.HasGravity() {
|
||||
velocity.X = 0
|
||||
velocity.Y = 0
|
||||
|
||||
// Shift to slow your roll to 1 pixel per tick.
|
||||
if keybind.Shift(ev) {
|
||||
playerSpeed = 1
|
||||
}
|
||||
|
||||
if keybind.Left(ev) {
|
||||
velocity.X = -playerSpeed
|
||||
} else if keybind.Right(ev) {
|
||||
velocity.X = playerSpeed
|
||||
}
|
||||
if keybind.Up(ev) {
|
||||
velocity.Y = -playerSpeed
|
||||
} else if keybind.Down(ev) {
|
||||
velocity.Y = playerSpeed
|
||||
}
|
||||
} else {
|
||||
// Moving left or right.
|
||||
if keybind.Left(ev) {
|
||||
direction = -1
|
||||
} else if keybind.Right(ev) {
|
||||
direction = 1
|
||||
}
|
||||
|
||||
// Up button to signal they want to jump.
|
||||
if keybind.Up(ev) {
|
||||
if s.Player.Grounded() {
|
||||
velocity.Y = balance.PlayerJumpVelocity
|
||||
}
|
||||
} else if velocity.Y < 0 {
|
||||
velocity.Y = 0
|
||||
}
|
||||
// if keybind.Up(ev) && (s.Player.Grounded() || s.playerJumpCounter >= 0) {
|
||||
// jumping = true
|
||||
|
||||
// if s.Player.Grounded() {
|
||||
// // Allow them to sustain the jump this many ticks.
|
||||
// s.playerJumpCounter = 32
|
||||
// }
|
||||
// }
|
||||
|
||||
// Moving left or right? Interpolate their velocity by acceleration.
|
||||
if direction != 0 {
|
||||
if s.playerLastDirection != direction {
|
||||
velocity.X = 0
|
||||
}
|
||||
|
||||
// TODO: fast turn-around if they change directions so they don't
|
||||
// slip and slide while their velocity updates.
|
||||
velocity.X = physics.Lerp(
|
||||
velocity.X,
|
||||
direction*s.playerPhysics.MaxSpeed.X,
|
||||
s.playerPhysics.Acceleration,
|
||||
)
|
||||
} else {
|
||||
// Slow them back to zero using friction.
|
||||
velocity.X = physics.Lerp(
|
||||
velocity.X,
|
||||
0,
|
||||
s.playerPhysics.Friction,
|
||||
)
|
||||
}
|
||||
|
||||
// Moving upwards (jumping): give them full acceleration upwards.
|
||||
if jumping {
|
||||
velocity.Y = -playerSpeed
|
||||
}
|
||||
|
||||
// While in the air, count down their jump counter; when zero they
|
||||
// cannot jump again until they touch ground.
|
||||
if !s.Player.Grounded() {
|
||||
s.playerJumpCounter--
|
||||
}
|
||||
}
|
||||
|
||||
s.playerLastDirection = direction
|
||||
|
||||
// Move the player unless frozen.
|
||||
// TODO: if Y=0 then gravity fails, but not doing this allows the
|
||||
// player to jump while frozen. Not a HUGE deal right now as only Warp Doors
|
||||
// freeze the player currently but do address this later.
|
||||
if s.Player.IsFrozen() {
|
||||
velocity.X = 0
|
||||
}
|
||||
s.Player.SetVelocity(velocity)
|
||||
|
||||
// If the "Use" key is pressed, set an actor flag on the player.
|
||||
s.Player.SetUsing(keybind.Use(ev))
|
||||
|
||||
s.scripting.To(s.Player.ID()).Events.RunKeypress(keybind.FromEvent(ev))
|
||||
}
|
||||
|
||||
// Drawing returns the private world drawing, for debugging with the console.
|
||||
func (s *PlayScene) Drawing() *uix.Canvas {
|
||||
return s.drawing
|
||||
|
|
119
pkg/player_physics.go
Normal file
119
pkg/player_physics.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package doodle
|
||||
|
||||
// Subset of the PlayScene that is responsible for movement of the player character.
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||
"git.kirsle.net/apps/doodle/pkg/physics"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
"git.kirsle.net/go/render/event"
|
||||
)
|
||||
|
||||
// movePlayer updates the player's X,Y coordinate based on key pressed.
|
||||
func (s *PlayScene) movePlayer(ev *event.State) {
|
||||
var (
|
||||
playerSpeed = float64(balance.PlayerMaxVelocity)
|
||||
velocity = s.Player.Velocity()
|
||||
direction float64
|
||||
jumping bool
|
||||
// holdingJump bool // holding down the jump button vs. tapping it
|
||||
)
|
||||
|
||||
// Antigravity: player can move anywhere with arrow keys.
|
||||
if s.antigravity || !s.Player.HasGravity() {
|
||||
velocity.X = 0
|
||||
velocity.Y = 0
|
||||
|
||||
// Shift to slow your roll to 1 pixel per tick.
|
||||
if keybind.Shift(ev) {
|
||||
playerSpeed = 1
|
||||
}
|
||||
|
||||
if keybind.Left(ev) {
|
||||
velocity.X = -playerSpeed
|
||||
} else if keybind.Right(ev) {
|
||||
velocity.X = playerSpeed
|
||||
}
|
||||
if keybind.Up(ev) {
|
||||
velocity.Y = -playerSpeed
|
||||
} else if keybind.Down(ev) {
|
||||
velocity.Y = playerSpeed
|
||||
}
|
||||
} else {
|
||||
// Moving left or right.
|
||||
if keybind.Left(ev) {
|
||||
direction = -1
|
||||
} else if keybind.Right(ev) {
|
||||
direction = 1
|
||||
}
|
||||
|
||||
// Up button to signal they want to jump.
|
||||
if keybind.Up(ev) {
|
||||
if s.Player.IsWet() {
|
||||
// If they are holding Up put a cooldown in how fast they can swim
|
||||
// to the surface. Tapping the Jump button allows a faster ascent.
|
||||
if shmem.Tick > s.jumpCooldownUntil {
|
||||
s.jumpCooldownUntil = shmem.Tick + balance.SwimJumpCooldown
|
||||
velocity.Y = balance.SwimJumpVelocity
|
||||
}
|
||||
} else if s.Player.Grounded() {
|
||||
velocity.Y = balance.PlayerJumpVelocity
|
||||
}
|
||||
} else {
|
||||
s.jumpCooldownUntil = 0
|
||||
if velocity.Y < 0 {
|
||||
velocity.Y = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Moving left or right? Interpolate their velocity by acceleration.
|
||||
if direction != 0 {
|
||||
if s.playerLastDirection != direction {
|
||||
velocity.X = 0
|
||||
}
|
||||
|
||||
// TODO: fast turn-around if they change directions so they don't
|
||||
// slip and slide while their velocity updates.
|
||||
velocity.X = physics.Lerp(
|
||||
velocity.X,
|
||||
direction*s.playerPhysics.MaxSpeed.X,
|
||||
s.playerPhysics.Acceleration,
|
||||
)
|
||||
} else {
|
||||
// Slow them back to zero using friction.
|
||||
velocity.X = physics.Lerp(
|
||||
velocity.X,
|
||||
0,
|
||||
s.playerPhysics.Friction,
|
||||
)
|
||||
}
|
||||
|
||||
// Moving upwards (jumping): give them full acceleration upwards.
|
||||
if jumping {
|
||||
velocity.Y = -playerSpeed
|
||||
}
|
||||
|
||||
// While in the air, count down their jump counter; when zero they
|
||||
// cannot jump again until they touch ground.
|
||||
if !s.Player.Grounded() {
|
||||
s.playerJumpCounter--
|
||||
}
|
||||
}
|
||||
|
||||
s.playerLastDirection = direction
|
||||
|
||||
// Move the player unless frozen.
|
||||
// TODO: if Y=0 then gravity fails, but not doing this allows the
|
||||
// player to jump while frozen. Not a HUGE deal right now as only Warp Doors
|
||||
// freeze the player currently but do address this later.
|
||||
if s.Player.IsFrozen() {
|
||||
velocity.X = 0
|
||||
}
|
||||
s.Player.SetVelocity(velocity)
|
||||
|
||||
// If the "Use" key is pressed, set an actor flag on the player.
|
||||
s.Player.SetUsing(keybind.Use(ev))
|
||||
|
||||
s.scripting.To(s.Player.ID()).Events.RunKeypress(keybind.FromEvent(ev))
|
||||
}
|
|
@ -36,6 +36,7 @@ type Actor struct {
|
|||
// Actor runtime variables.
|
||||
hasGravity bool
|
||||
hasInventory bool
|
||||
wet bool
|
||||
isMobile bool // Mobile character, such as the player or an enemy
|
||||
noclip bool // Disable collision detection
|
||||
hidden bool // invisible, via Hide() and Show()
|
||||
|
@ -152,6 +153,16 @@ func (a *Actor) SetInvulnerable(v bool) {
|
|||
a.immortal = v
|
||||
}
|
||||
|
||||
// Wet returns whether the actor is in contact with water pixels in a level.
|
||||
func (a *Actor) IsWet() bool {
|
||||
return a.wet
|
||||
}
|
||||
|
||||
// SetWet updates the state of the actor's wet-ness.
|
||||
func (a *Actor) SetWet(v bool) {
|
||||
a.wet = v
|
||||
}
|
||||
|
||||
// Size returns the size of the actor, from the underlying doodads.Drawing.
|
||||
func (a *Actor) Size() render.Rect {
|
||||
return a.Drawing.Size()
|
||||
|
|
|
@ -73,9 +73,13 @@ func (w *Canvas) loopActorCollision() error {
|
|||
// Apply gravity to the actor's velocity.
|
||||
if a.hasGravity && !a.Grounded() { //v.Y >= 0 {
|
||||
if !a.Grounded() {
|
||||
var gravity = balance.Gravity
|
||||
if a.IsWet() {
|
||||
gravity = balance.SwimGravity
|
||||
}
|
||||
v.Y = physics.Lerp(
|
||||
v.Y, // current speed
|
||||
balance.Gravity, // target max gravity falling downwards
|
||||
gravity, // target max gravity falling downwards
|
||||
balance.GravityAcceleration,
|
||||
)
|
||||
} else {
|
||||
|
@ -98,13 +102,12 @@ func (w *Canvas) loopActorCollision() error {
|
|||
|
||||
// Check collision with level geometry.
|
||||
chkPoint := delta.ToPoint()
|
||||
info, ok := collision.CollidesWithGrid(a, w.chunks, chkPoint)
|
||||
if ok {
|
||||
// Collision happened with world.
|
||||
info, _ := collision.CollidesWithGrid(a, w.chunks, chkPoint)
|
||||
|
||||
// Inform the caller about the collision state every tick
|
||||
if w.OnLevelCollision != nil {
|
||||
w.OnLevelCollision(a, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Move us back where the collision check put us
|
||||
if !a.noclip {
|
||||
|
|
|
@ -122,6 +122,7 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
|
|||
"Destroy": actor.Destroy,
|
||||
"Freeze": actor.Freeze,
|
||||
"Unfreeze": actor.Unfreeze,
|
||||
"IsWet": actor.IsWet,
|
||||
"Hide": actor.Hide,
|
||||
"Show": actor.Show,
|
||||
"GetLinks": func() []map[string]interface{} {
|
||||
|
|
Loading…
Reference in New Issue
Block a user