Files
sanspie 6e8ebcd90f
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 5m39s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m49s
ci / release (push) Successful in 7m23s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
docs: the scenes docs check plays after each step stay out of the pages
2026-10-25 18:13:09 +03:00

16 KiB
Raw Permalink Blame History

id, title
id title
recipes Рецепты

Рецепты

Короткие ответы на частые вопросы о платформерах. Каждый рецепт — это скрипт и сцена, в которой он работает, на основе игрока из руководства Платформер с нуля; каждый из них запускается проверками документации.

Двойной прыжок

«Движение 2D» прыгает только с земли или в пределах «Времени койота». Второй прыжок в воздухе задаёт скорость вверх напрямую, а счётчик сбрасывается при приземлении:

package game

import "aego/input"

script Player {
	@export
	var AirJumps int = 1

	@export
	var AirJumpSpeed float = 8

	var left int

	func Ready() {
		node.Get<Motion2D>().Landed.Connect(OnLanded)
	}

	func OnLanded(speed float) {
		left = AirJumps
	}

	func Update(dt float) {
		m := node.Get<Motion2D>()
		m.Move(input.Axis("Move"))
		if input.Released("Jump") {
			m.JumpReleased()
		}
		if input.Pressed("Jump") {
			if m.Grounded {
				m.Jump()
			} else if left > 0 {
				left--
				m.AddVelocity(Vec2{0, -m.Velocity.Y - AirJumpSpeed})
			}
		}
	}
}

Скорость задаётся в единицах в секунду, а вверх — это отрицательный Y, поэтому добавленная скорость гасит текущую вертикальную скорость и задаёт AirJumpSpeed вверх.

scene version=1 {
  node "e1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
    transform {
      position=[0, 3]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#4b5563ff
      size=[40, 1]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.BoxCollider2D" version=1 {
      size=[40, 1]
    }
  }
  node "e1000000000000000000000000000002" name="Player" enabled=true layers=1 {
    component "engine.SpriteRenderer" version=1 {
      tint=#f59e0bff
      size=[0.6, 1]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
      fixedRotation=true
    }
    component "physics.BoxCollider2D" version=1 {
      friction=0
      size=[0.6, 1]
    }
    component "gameplay.Motion2D" version=1 {
      mode=platformer
    }
    component "engine.Script" version=1 {
      script "game.game.Player" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
    component "engine.Camera2D" version=1 {
      zoom=36
    }
    component "camera.VirtualCamera2D" version=1 {
    }
    component "camera.CameraFollow2D" version=1 {
      target=node("e1000000000000000000000000000002")
    }
  }
}

Односторонние платформы

Платформе, через которую игрок запрыгивает снизу и на которой стоит сверху, скрипт не нужен. Дайте ей «Коллайдер-линию 2D» с двумя точками и включённым флагом «Односторонний». Скрипт игрока — обычный, из руководства:

package game

import "aego/input"

script Player {
	func Update(dt float) {
		m := node.Get<Motion2D>()
		m.Move(input.Axis("Move"))
		if input.Released("Jump") {
			m.JumpReleased()
		}
		if input.Pressed("Jump") {
			m.Jump()
		}
	}
}
scene version=1 {
  node "e1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
    transform {
      position=[0, 3]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#4b5563ff
      size=[40, 1]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.BoxCollider2D" version=1 {
      size=[40, 1]
    }
  }
  node "e1000000000000000000000000000004" name="Ledge" enabled=true layers=1 {
    transform {
      position=[2, 0.8]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#94a3b8ff
      size=[3, 0.2]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.EdgeCollider2D" version=1 {
      points=[[-1.5, -0.1], [1.5, -0.1]]
      oneSided=true
    }
  }
  node "e1000000000000000000000000000002" name="Player" enabled=true layers=1 {
    component "engine.SpriteRenderer" version=1 {
      tint=#f59e0bff
      size=[0.6, 1]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
      fixedRotation=true
    }
    component "physics.BoxCollider2D" version=1 {
      friction=0
      size=[0.6, 1]
    }
    component "gameplay.Motion2D" version=1 {
      mode=platformer
    }
    component "engine.Script" version=1 {
      script "game.game.Player" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
    component "engine.Camera2D" version=1 {
      zoom=36
    }
    component "camera.VirtualCamera2D" version=1 {
    }
    component "camera.CameraFollow2D" version=1 {
      target=node("e1000000000000000000000000000002")
    }
  }
}

Движущиеся платформы

Кинематическое тело, которое двигается через MoveTo, везёт стоящее на нём «Движение 2D», пока включены «Движущиеся платформы»; по умолчанию они включены. Этот скрипт двигает своё тело туда и обратно между начальной точкой и Travel:

package game

import "aego/math"

script Lift {
	@export
	var Travel Vec2 = Vec2{4, 0}

	@export
	var Period float = 4

	var t float
	var start Vec2

	func Ready() {
		start = node.Position()
	}

	func FixedUpdate(dt float) {
		t += dt
		k := 0.5 - 0.5*math.Cos(t/Period*2*math.Pi)
		node.Get<Body2D>().MoveTo(Vec2{start.X + Travel.X*k, start.Y + Travel.Y*k}, 0)
	}
}
scene version=1 {
  node "e1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
    transform {
      position=[0, 3]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#4b5563ff
      size=[40, 1]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.BoxCollider2D" version=1 {
      size=[40, 1]
    }
  }
  node "e1000000000000000000000000000005" name="Lift" enabled=true layers=1 {
    transform {
      position=[3, 1.4]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#0ea5e9ff
      size=[2, 0.3]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
    }
    component "physics.BoxCollider2D" version=1 {
      size=[2, 0.3]
    }
    component "engine.Script" version=1 {
      script "game.game.Lift" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000002" name="Player" enabled=true layers=1 {
    component "engine.SpriteRenderer" version=1 {
      tint=#f59e0bff
      size=[0.6, 1]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
      fixedRotation=true
    }
    component "physics.BoxCollider2D" version=1 {
      friction=0
      size=[0.6, 1]
    }
    component "gameplay.Motion2D" version=1 {
      mode=platformer
    }
    component "engine.Script" version=1 {
      script "game.game.Player" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
    component "engine.Camera2D" version=1 {
      zoom=36
    }
    component "camera.VirtualCamera2D" version=1 {
    }
    component "camera.CameraFollow2D" version=1 {
      target=node("e1000000000000000000000000000002")
    }
  }
}

«Платформа при уходе» в «Движении 2D» игрока определяет, что происходит со скоростью платформы, когда игрок с неё спрыгивает.

Рывок

Dash двигает с постоянной скоростью в течение заданного времени и всё это время не учитывает ввод и гравитацию, если не включена «Гравитация в рывке». Перезарядка не даёт повторять рывок без остановки:

package game

import "aego/input"

script Player {
	@export
	var DashSpeed float = 16

	@export
	var DashTime float = 0.15

	@export
	var Cooldown float = 0.6

	var facing float = 1
	var wait float

	func Update(dt float) {
		m := node.Get<Motion2D>()
		m.Move(input.Axis("Move"))
		if input.Released("Jump") {
			m.JumpReleased()
		}
		if input.Pressed("Jump") {
			m.Jump()
		}
		x := input.Axis("Move")
		if x != 0 {
			facing = x
		}
		wait -= dt
		if input.KeyPressed(input.KeyLShift) && wait <= 0 {
			m.Dash(Vec2{facing * DashSpeed, 0}, DashTime)
			wait = Cooldown
		}
	}
}

Здесь клавиша читается напрямую. В игре добавьте действие Dash в Настройках проекта → Ввод и читайте его через input.Pressed("Dash"), чтобы его можно было переназначить и чтобы оно работало на геймпаде.

scene version=1 {
  node "e1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
    transform {
      position=[0, 3]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#4b5563ff
      size=[40, 1]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.BoxCollider2D" version=1 {
      size=[40, 1]
    }
  }
  node "e1000000000000000000000000000002" name="Player" enabled=true layers=1 {
    component "engine.SpriteRenderer" version=1 {
      tint=#f59e0bff
      size=[0.6, 1]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
      fixedRotation=true
    }
    component "physics.BoxCollider2D" version=1 {
      friction=0
      size=[0.6, 1]
    }
    component "gameplay.Motion2D" version=1 {
      mode=platformer
    }
    component "engine.Script" version=1 {
      script "game.game.Player" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
    component "engine.Camera2D" version=1 {
      zoom=36
    }
    component "camera.VirtualCamera2D" version=1 {
    }
    component "camera.CameraFollow2D" version=1 {
      target=node("e1000000000000000000000000000002")
    }
  }
}

Тряска камеры при жёстком приземлении

Landed передаёт скорость падения. Трясите камеру, только когда эта скорость велика, чтобы обычные подскоки оставались спокойными. Камере нужна «Тряска камеры 2D»:

package game

import (
	"aego/camera"
	"aego/input"
)

script Player {
	@export
	var HardLanding float = 12

	func Ready() {
		node.Get<Motion2D>().Landed.Connect(OnLanded)
	}

	func OnLanded(speed float) {
		if speed > HardLanding {
			camera.Shake(0.4)
		}
	}

	func Update(dt float) {
		m := node.Get<Motion2D>()
		m.Move(input.Axis("Move"))
		if input.Released("Jump") {
			m.JumpReleased()
		}
		if input.Pressed("Jump") {
			m.Jump()
		}
	}
}

Тряска добавляет «травму» от 0 до 1; «Тряска камеры 2D» превращает её в движение и даёт ей затухнуть. Игроки могут ослабить или выключить тряску в настройках доступности.

scene version=1 {
  node "e1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
    transform {
      position=[0, 3]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#4b5563ff
      size=[40, 1]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.BoxCollider2D" version=1 {
      size=[40, 1]
    }
  }
  node "e1000000000000000000000000000002" name="Player" enabled=true layers=1 {
    component "engine.SpriteRenderer" version=1 {
      tint=#f59e0bff
      size=[0.6, 1]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
      fixedRotation=true
    }
    component "physics.BoxCollider2D" version=1 {
      friction=0
      size=[0.6, 1]
    }
    component "gameplay.Motion2D" version=1 {
      mode=platformer
    }
    component "engine.Script" version=1 {
      script "game.game.Player" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
    component "engine.Camera2D" version=1 {
      zoom=36
    }
    component "camera.VirtualCamera2D" version=1 {
    }
    component "camera.CameraFollow2D" version=1 {
      target=node("e1000000000000000000000000000002")
    }
    component "camera.CameraShake2D" version=1 {
    }
  }
}

Прыжок на врага

Прыжок на голову врага — это хитбокс на ногах игрока, активный только во время падения, и хёртбокс на враге. Отскок даёт сигнал Hit хитбокса:

package game

import "aego/input"

script Player {
	@export
	var Bounce float = 9

	func Ready() {
		node.Get<Hitbox2D>().Hit.Connect(OnHit)
	}

	func OnHit(target NodeRef, damage int) {
		m := node.Get<Motion2D>()
		m.AddVelocity(Vec2{0, -m.Velocity.Y - Bounce})
	}

	func FixedUpdate(dt float) {
		m := node.Get<Motion2D>()
		node.Get<Hitbox2D>().Active = !m.Grounded && m.Velocity.Y > 0
	}

	func Update(dt float) {
		m := node.Get<Motion2D>()
		m.Move(input.Axis("Move"))
		if input.Released("Jump") {
			m.JumpReleased()
		}
		if input.Pressed("Jump") {
			m.Jump()
		}
	}
}
scene version=1 {
  node "e1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
    transform {
      position=[0, 3]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#4b5563ff
      size=[40, 1]
    }
    component "physics.Body2D" version=1 {
      type=static
    }
    component "physics.BoxCollider2D" version=1 {
      size=[40, 1]
    }
  }
  node "e1000000000000000000000000000006" name="Enemy" enabled=true layers=1 {
    transform {
      position=[4, 2.1]
    }
    component "engine.SpriteRenderer" version=1 {
      tint=#ef4444ff
      size=[0.8, 0.8]
    }
    component "gameplay.Health" version=1 {
      max=1
      team=1
      destroyOnDeath=true
    }
    component "gameplay.Hurtbox2D" version=1 {
      area=[-0.4, -0.4, 0.8, 0.8]
    }
  }
  node "e1000000000000000000000000000002" name="Player" enabled=true layers=1 {
    component "engine.SpriteRenderer" version=1 {
      tint=#f59e0bff
      size=[0.6, 1]
    }
    component "physics.Body2D" version=1 {
      type=kinematic
      fixedRotation=true
    }
    component "physics.BoxCollider2D" version=1 {
      friction=0
      size=[0.6, 1]
    }
    component "gameplay.Motion2D" version=1 {
      mode=platformer
    }
    component "gameplay.Health" version=1 {
      max=3
    }
    component "gameplay.Hitbox2D" version=1 {
      area=[-0.25, 0.35, 0.5, 0.25]
      damage=1
      active=false
    }
    component "engine.Script" version=1 {
      script "game.game.Player" enabled=true {
      }
    }
  }
  node "e1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
    component "engine.Camera2D" version=1 {
      zoom=36
    }
    component "camera.VirtualCamera2D" version=1 {
    }
    component "camera.CameraFollow2D" version=1 {
      target=node("e1000000000000000000000000000002")
    }
  }
}

«Здоровье» игрока ставит его в команду 0, а «Здоровье» врага — в команду 1. Хитбокс на ногах берёт команду ближайшего «Здоровья» над ним, поэтому ранит врага и никогда не ранит игрока. examples/platformer использует ту же схему.

См. также