Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54693f97d2 | ||
|
|
080948f434 |
@@ -272,6 +272,7 @@ check-all: cli
|
|||||||
.cache/bin/aego check examples/native-rotator
|
.cache/bin/aego check examples/native-rotator
|
||||||
.cache/bin/aego check examples/platformer
|
.cache/bin/aego check examples/platformer
|
||||||
.cache/bin/aego test examples/platformer
|
.cache/bin/aego test examples/platformer
|
||||||
|
.cache/bin/aego check examples/topdown
|
||||||
.cache/bin/aego check examples/native-settings
|
.cache/bin/aego check examples/native-settings
|
||||||
.cache/bin/aego check examples/native-editor
|
.cache/bin/aego check examples/native-editor
|
||||||
.cache/bin/aego check app/playground/examples
|
.cache/bin/aego check app/playground/examples
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 126 KiB |
@@ -80,7 +80,7 @@ aego edit mygame/scenes/main.aescene
|
|||||||
aego edit --launcher new
|
aego edit --launcher new
|
||||||
```
|
```
|
||||||
|
|
||||||
`aego new` creates a 2D project with a scene holding a camera and a sprite, and a script to start from; `--template 3d` and the other templates are listed by `aego help`. In the editor, the Getting Started panel walks through the first six steps: a node, a script, Play, a live tweak, hot reload and fixing an error.
|
`aego new` creates a 2D project with a scene holding a camera and a sprite, and a script to start from; `--template 3d` and the other templates are listed by `aego help`. `aego new mygame --example platformer` copies the platformer (a character on Motion 2D with stomps, a pit, a checkpoint and save games) and `--example topdown` the top-down game (a sword, enemies that patrol and chase with a State Machine, coins and lava); the launcher lists the same examples under **Examples**. In the editor, the Getting Started panel walks through the first six steps: a node, a script, Play, a live tweak, hot reload and fixing an error.
|
||||||
|
|
||||||
| Command | Does |
|
| Command | Does |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
@@ -5,12 +5,6 @@ import (
|
|||||||
"aego/sdk"
|
"aego/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Health struct {
|
|
||||||
Max float32 `aego:"max,min=1"`
|
|
||||||
Current float32 `aego:"current,min=0"`
|
|
||||||
Hits int64 `aego:"hits"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnemyStats struct {
|
type EnemyStats struct {
|
||||||
Speed float32 `aego:"speed,min=0,max=10"`
|
Speed float32 `aego:"speed,min=0,max=10"`
|
||||||
Cooldown float32 `aego:"cooldown,min=0,max=5"`
|
Cooldown float32 `aego:"cooldown,min=0,max=5"`
|
||||||
@@ -25,38 +19,10 @@ func (Module) Info() sdk.ModuleInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (Module) Register(r *sdk.Registry) error {
|
func (Module) Register(r *sdk.Registry) error {
|
||||||
if err := sdk.Component[Health](r, sdk.Name("game.Health"), sdk.Doc("hit points of a character"), sdk.Persist("current", "hits")).Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := sdk.DataType[EnemyStats](r, sdk.Name("mygame.EnemyStats"), sdk.Doc("movement and attack numbers of one kind of enemy")).Err(); err != nil {
|
if err := sdk.DataType[EnemyStats](r, sdk.Name("mygame.EnemyStats"), sdk.Doc("movement and attack numbers of one kind of enemy")).Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sdk.Live[EnemyStats](r, "speed")
|
sdk.Live[EnemyStats](r, "speed")
|
||||||
m := r.ScriptModule("mygame/combat")
|
|
||||||
sdk.Proc2(m, "Damage", func(c *sdk.ScriptCtx, target sdk.NodeHandle, amount int64) {
|
|
||||||
if c.Scene == nil || !c.Scene.IsAlive(target) {
|
|
||||||
c.Fail("damage target is nil or destroyed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h, ok := sdk.Get[Health](c.Scene, target)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.Current -= float32(amount)
|
|
||||||
h.Hits++
|
|
||||||
if h.Current <= 0 {
|
|
||||||
h.Current = h.Max
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sdk.Func1(m, "HealthOf", func(c *sdk.ScriptCtx, target sdk.NodeHandle) float32 {
|
|
||||||
if c.Scene == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if h, ok := sdk.Get[Health](c.Scene, target); ok {
|
|
||||||
return h.Current
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,19 @@ prefab version=1 {
|
|||||||
density=1
|
density=1
|
||||||
size=[0.9, 0.9]
|
size=[0.9, 0.9]
|
||||||
}
|
}
|
||||||
component "physics.PolygonCollider2D" version=1 {
|
component "gameplay.Health" version=1 {
|
||||||
offset=[0, 0]
|
max=1
|
||||||
isTrigger=true
|
team=1
|
||||||
friction=0.6
|
invulnTime=0
|
||||||
restitution=0
|
}
|
||||||
density=0
|
component "gameplay.Hurtbox2D" version=1 {
|
||||||
points=[[-0.35, -0.85], [0.35, -0.85], [0.35, -0.65], [-0.35, -0.65]]
|
area=[-0.4, -0.45, 0.8, 0.35]
|
||||||
|
}
|
||||||
|
component "gameplay.Hitbox2D" version=1 {
|
||||||
|
area=[-0.45, -0.1, 0.9, 0.55]
|
||||||
|
policy=periodic
|
||||||
|
knockback=[7, 0]
|
||||||
|
knockbackFromCenter=true
|
||||||
}
|
}
|
||||||
component "save.Persist" version=1 {
|
component "save.Persist" version=1 {
|
||||||
noTransform=false
|
noTransform=false
|
||||||
@@ -55,7 +61,6 @@ prefab version=1 {
|
|||||||
script "mygame.enemy.EnemyAI" enabled=true {
|
script "mygame.enemy.EnemyAI" enabled=true {
|
||||||
PointA=node("00000000000000000000000000000000")
|
PointA=node("00000000000000000000000000000000")
|
||||||
PointB=node("00000000000000000000000000000000")
|
PointB=node("00000000000000000000000000000000")
|
||||||
Target=node("00000000000000000000000000000000")
|
|
||||||
Stats=asset("da7a0000000000000000000000000001")
|
Stats=asset("da7a0000000000000000000000000001")
|
||||||
DieSound=asset("a0d10000000000000000000000000013")
|
DieSound=asset("a0d10000000000000000000000000013")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ package enemy
|
|||||||
import (
|
import (
|
||||||
"aego/audio"
|
"aego/audio"
|
||||||
"aego/curve"
|
"aego/curve"
|
||||||
"aego/physics"
|
|
||||||
"aego/timer"
|
|
||||||
"aego/tween"
|
"aego/tween"
|
||||||
"mygame/combat"
|
|
||||||
"mygame/shared"
|
"mygame/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,9 +16,6 @@ script EnemyAI {
|
|||||||
@export
|
@export
|
||||||
var PointB NodeRef
|
var PointB NodeRef
|
||||||
|
|
||||||
@export
|
|
||||||
var Target NodeRef
|
|
||||||
|
|
||||||
@export(tip="Speed, cooldown, damage and tint of this kind of enemy")
|
@export(tip="Speed, cooldown, damage and tint of this kind of enemy")
|
||||||
var Stats AssetRef<EnemyStats>
|
var Stats AssetRef<EnemyStats>
|
||||||
|
|
||||||
@@ -29,16 +23,15 @@ script EnemyAI {
|
|||||||
var DieSound AssetRef<AudioEvent>
|
var DieSound AssetRef<AudioEvent>
|
||||||
|
|
||||||
var toB bool = true
|
var toB bool = true
|
||||||
var hit timer.Timer
|
|
||||||
var dead bool
|
var dead bool
|
||||||
var fade Connection
|
var fade Connection
|
||||||
|
|
||||||
func Ready() {
|
func Ready() {
|
||||||
hit = timer.Cooldown(Stats.Cooldown)
|
node.Get<SpriteRenderer>().Tint = Stats.Tint
|
||||||
sprite := node.Get<SpriteRenderer>()
|
hit := node.Get<Hitbox2D>()
|
||||||
if sprite != nil {
|
hit.Damage = Stats.Damage
|
||||||
sprite.Tint = Stats.Tint
|
hit.Period = Stats.Cooldown
|
||||||
}
|
node.Get<Health>().Died.Connect(OnDied)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FixedUpdate(dt float) {
|
func FixedUpdate(dt float) {
|
||||||
@@ -46,9 +39,6 @@ script EnemyAI {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
body := node.Get<Body2D>()
|
body := node.Get<Body2D>()
|
||||||
if body == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
goal := PointA.Position()
|
goal := PointA.Position()
|
||||||
if toB {
|
if toB {
|
||||||
goal = PointB.Position()
|
goal = PointB.Position()
|
||||||
@@ -62,15 +52,10 @@ script EnemyAI {
|
|||||||
body.SetVelocity(v)
|
body.SetVelocity(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OnTriggerEnter(other NodeRef) {
|
// OnDied stops the attack, tells the player and fades the enemy out.
|
||||||
if dead || other != Target {
|
func OnDied(source NodeRef) {
|
||||||
return
|
|
||||||
}
|
|
||||||
tb := other.Get<Body2D>()
|
|
||||||
if tb == nil || tb.Velocity.Y <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dead = true
|
dead = true
|
||||||
|
node.Get<Hitbox2D>().Deactivate()
|
||||||
Died.Emit()
|
Died.Emit()
|
||||||
audio.PlayEventAt(DieSound, node.Position())
|
audio.PlayEventAt(DieSound, node.Position())
|
||||||
fade = tween.ToFloat(node, "SpriteRenderer.Tint.a", 0, 0.2, curve.OutQuad).Completed.Connect(OnFaded)
|
fade = tween.ToFloat(node, "SpriteRenderer.Tint.a", 0, 0.2, curve.OutQuad).Completed.Connect(OnFaded)
|
||||||
@@ -79,11 +64,4 @@ script EnemyAI {
|
|||||||
func OnFaded(finished bool) {
|
func OnFaded(finished bool) {
|
||||||
node.Destroy()
|
node.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
func OnCollisionEnter(other NodeRef, c physics.Contact) {
|
|
||||||
if dead || other != Target || c.Normal.Y > 0.5 || !hit.Use() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
combat.Damage(Target, Stats.Damage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.aego/
|
||||||
|
exports/
|
||||||
|
After Width: | Height: | Size: 596 B |
@@ -0,0 +1,67 @@
|
|||||||
|
project version=1 {
|
||||||
|
name="Top-down"
|
||||||
|
module="topdown"
|
||||||
|
mainScene=asset("70d0000000000000000000000000a001")
|
||||||
|
native=false
|
||||||
|
window {
|
||||||
|
width=1280
|
||||||
|
height=720
|
||||||
|
title="Aego Top-down"
|
||||||
|
}
|
||||||
|
input version=2 {
|
||||||
|
stack=[Gameplay]
|
||||||
|
action {
|
||||||
|
name=MoveX
|
||||||
|
type=axis1
|
||||||
|
bind=["Key.A/Key.D", "Key.Left/Key.Right", "Pad.LeftX"]
|
||||||
|
}
|
||||||
|
action {
|
||||||
|
name=MoveY
|
||||||
|
type=axis1
|
||||||
|
bind=["Key.W/Key.S", "Key.Up/Key.Down", "Pad.LeftY"]
|
||||||
|
}
|
||||||
|
action {
|
||||||
|
name=AimX
|
||||||
|
type=axis1
|
||||||
|
bind=["Pad.RightX"]
|
||||||
|
}
|
||||||
|
action {
|
||||||
|
name=AimY
|
||||||
|
type=axis1
|
||||||
|
bind=["Pad.RightY"]
|
||||||
|
}
|
||||||
|
action {
|
||||||
|
name=Attack
|
||||||
|
type=button
|
||||||
|
bind=["Key.J", "Mouse.Left", "Pad.X"]
|
||||||
|
}
|
||||||
|
action {
|
||||||
|
name=Pause
|
||||||
|
type=button
|
||||||
|
bind=["Key.Escape", "Pad.Start"]
|
||||||
|
}
|
||||||
|
action {
|
||||||
|
name=Back
|
||||||
|
type=button
|
||||||
|
bind=["Key.Escape", "Pad.B"]
|
||||||
|
}
|
||||||
|
context {
|
||||||
|
name=Gameplay
|
||||||
|
actions=[MoveX, MoveY, AimX, AimY, Attack, Pause]
|
||||||
|
}
|
||||||
|
context {
|
||||||
|
name=Menu
|
||||||
|
priority=10
|
||||||
|
consume=true
|
||||||
|
actions=[Back]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
physics {
|
||||||
|
gravity=[0, 0]
|
||||||
|
layers=[Default, Walls, Player, Enemy]
|
||||||
|
collide {
|
||||||
|
Player=[Walls, Enemy]
|
||||||
|
Enemy=[Walls, Player]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
meta version=1 {
|
||||||
|
id="70d0000000000000000000000000a001"
|
||||||
|
importer="scene"
|
||||||
|
importerVersion=1
|
||||||
|
settings {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package enemy
|
||||||
|
|
||||||
|
import "aego/math"
|
||||||
|
|
||||||
|
// Grunt patrols between two points and chases the player while it is close; its State Machine runs FixedPatrol or FixedChase.
|
||||||
|
script Grunt {
|
||||||
|
@export
|
||||||
|
var Player NodeRef
|
||||||
|
|
||||||
|
@export
|
||||||
|
var PointA NodeRef
|
||||||
|
|
||||||
|
@export
|
||||||
|
var PointB NodeRef
|
||||||
|
|
||||||
|
@export(min=0)
|
||||||
|
var SeeRange float = 4
|
||||||
|
|
||||||
|
@export(min=0)
|
||||||
|
var LoseRange float = 6
|
||||||
|
|
||||||
|
var toB bool = true
|
||||||
|
|
||||||
|
func toPlayer() Vec2 {
|
||||||
|
return Player.WorldPosition() - node.WorldPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixedPatrol(dt float) {
|
||||||
|
goal := PointA.WorldPosition()
|
||||||
|
if toB {
|
||||||
|
goal = PointB.WorldPosition()
|
||||||
|
}
|
||||||
|
d := goal - node.WorldPosition()
|
||||||
|
if math.Length(d) < 0.2 {
|
||||||
|
toB = !toB
|
||||||
|
}
|
||||||
|
node.Get<Motion2D>().MoveDir(math.Normalize(d) * 0.5)
|
||||||
|
if Player != nil && Player.IsAlive() && math.Length(toPlayer()) < SeeRange {
|
||||||
|
node.Get<StateMachine>().Go("Chase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FixedChase(dt float) {
|
||||||
|
if Player == nil || !Player.IsAlive() || math.Length(toPlayer()) > LoseRange {
|
||||||
|
node.Get<StateMachine>().Go("Patrol")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node.Get<Motion2D>().MoveDir(math.Normalize(toPlayer()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnterChase() {
|
||||||
|
node.Get<SpriteRenderer>().Tint = Color{1, 0.35, 0.35, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitChase() {
|
||||||
|
node.Get<SpriteRenderer>().Tint = Color{0.75, 0.5, 0.5, 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
meta version=1 {
|
||||||
|
id="70d0000000000000000000000000a012"
|
||||||
|
importer="script"
|
||||||
|
importerVersion=1
|
||||||
|
settings {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package hud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aego/camera"
|
||||||
|
"aego/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Coins counts the collected coins and writes the count in the corner of the view.
|
||||||
|
script Coins {
|
||||||
|
@export
|
||||||
|
var C1 NodeRef
|
||||||
|
|
||||||
|
@export
|
||||||
|
var C2 NodeRef
|
||||||
|
|
||||||
|
@export
|
||||||
|
var C3 NodeRef
|
||||||
|
|
||||||
|
@export
|
||||||
|
var C4 NodeRef
|
||||||
|
|
||||||
|
var coins int
|
||||||
|
var k1 Connection
|
||||||
|
var k2 Connection
|
||||||
|
var k3 Connection
|
||||||
|
var k4 Connection
|
||||||
|
|
||||||
|
func Ready() {
|
||||||
|
k1 = C1.Get<Pickup2D>().Collected.Connect(OnCollected)
|
||||||
|
k2 = C2.Get<Pickup2D>().Collected.Connect(OnCollected)
|
||||||
|
k3 = C3.Get<Pickup2D>().Collected.Connect(OnCollected)
|
||||||
|
k4 = C4.Get<Pickup2D>().Collected.Connect(OnCollected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnCollected(by NodeRef) {
|
||||||
|
coins++
|
||||||
|
}
|
||||||
|
|
||||||
|
func Update(dt float) {
|
||||||
|
p := camera.LogicalPose().Position
|
||||||
|
debug.Text(Vec2{p.X - 12, p.Y - 6.5}, "coins " + string(coins) + " / 4", Color{1, 0.85, 0.2, 1})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
meta version=1 {
|
||||||
|
id="70d0000000000000000000000000a013"
|
||||||
|
importer="script"
|
||||||
|
importerVersion=1
|
||||||
|
settings {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aego/input"
|
||||||
|
"aego/math"
|
||||||
|
"aego/settings"
|
||||||
|
"aego/timer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hero moves with the left stick or WASD and swings its Sword towards the right stick, the mouse or the way it walks.
|
||||||
|
script Hero {
|
||||||
|
@export
|
||||||
|
var Sword NodeRef
|
||||||
|
|
||||||
|
@export(min=0)
|
||||||
|
var Reach float = 0.8
|
||||||
|
|
||||||
|
var facing Vec2 = Vec2{1, 0}
|
||||||
|
var swing timer.Timer
|
||||||
|
var mouse Vec2
|
||||||
|
var useMouse bool
|
||||||
|
|
||||||
|
func Ready() {
|
||||||
|
swing = timer.Cooldown(0.3)
|
||||||
|
mouse = input.MousePos()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Update(dt float) {
|
||||||
|
move := Vec2{input.Axis("MoveX"), input.Axis("MoveY")}
|
||||||
|
node.Get<Motion2D>().MoveDir(move)
|
||||||
|
aim := Vec2{input.Axis("AimX"), input.Axis("AimY")}
|
||||||
|
p := input.MousePos()
|
||||||
|
if math.Dist(p, mouse) > 2 {
|
||||||
|
mouse = p
|
||||||
|
useMouse = true
|
||||||
|
}
|
||||||
|
if math.Length(aim) > 0.3 {
|
||||||
|
facing = math.Normalize(aim)
|
||||||
|
useMouse = false
|
||||||
|
} else if useMouse {
|
||||||
|
to := input.MouseWorld() - node.WorldPosition()
|
||||||
|
if math.Length(to) > 0.2 {
|
||||||
|
facing = math.Normalize(to)
|
||||||
|
}
|
||||||
|
} else if math.Length(move) > 0.1 {
|
||||||
|
facing = math.Normalize(move)
|
||||||
|
}
|
||||||
|
Sword.SetPosition(facing * Reach)
|
||||||
|
Sword.SetRotation(math.Angle(facing))
|
||||||
|
if input.Pressed("Attack") && swing.Use() {
|
||||||
|
Sword.Get<Hitbox2D>().Pulse()
|
||||||
|
}
|
||||||
|
if input.Pressed("Pause") {
|
||||||
|
settings.OpenView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
meta version=1 {
|
||||||
|
id="70d0000000000000000000000000a011"
|
||||||
|
importer="script"
|
||||||
|
importerVersion=1
|
||||||
|
settings {}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ The gameplay module adds the parts most 2D games rebuild by hand: a character co
|
|||||||
|
|
||||||
Quick start: Create → Gameplay → Platformer Character gives a node with a kinematic Rigid Body 2D, a collider, Motion 2D, Health, Hurtbox 2D and Respawn. Give it a script that calls `Move` and `Jump`, add a floor and press Play.
|
Quick start: Create → Gameplay → Platformer Character gives a node with a kinematic Rigid Body 2D, a collider, Motion 2D, Health, Hurtbox 2D and Respawn. Give it a script that calls `Move` and `Jump`, add a floor and press Play.
|
||||||
|
|
||||||
|
Two complete games use the kit: `aego new mygame --example platformer` (stomping enemies with a foot hitbox, a pit with a kill zone, a checkpoint, a persisted respawn point and a state machine for the player's look) and `aego new mygame --example topdown` (a sword swung towards the stick, the mouse or the walking direction, enemies whose State Machine switches between Patrol and Chase, coins and lava). The launcher offers both under **Examples**.
|
||||||
|
|
||||||
| Component | Purpose |
|
| Component | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Motion 2D | walks, jumps and slides a kinematic body: slopes, steps, moving platforms, top-down movement |
|
| Motion 2D | walks, jumps and slides a kinematic body: slopes, steps, moving platforms, top-down movement |
|
||||||
|
|||||||
@@ -208,18 +208,18 @@ func TestApplyAndInverse(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrefabFlow(t *testing.T) {
|
func TestPrefabFlow(t *testing.T) {
|
||||||
e := open(t)
|
e := open(t)
|
||||||
out := e.must("prefab.create", agent.Args{"file": level, "node": "Ground", "out": "prefabs/ground"})
|
out := e.must("prefab.create", agent.Args{"file": level, "node": "GroundLeft", "out": "prefabs/ground"})
|
||||||
if len(out.Created) != 1 {
|
if len(out.Created) != 1 {
|
||||||
t.Fatalf("create %+v", out)
|
t.Fatalf("create %+v", out)
|
||||||
}
|
}
|
||||||
ins := e.must("prefab.instantiate", agent.Args{"file": level, "prefab": "prefabs/ground.aeprefab", "name": "Ground2"})
|
ins := e.must("prefab.instantiate", agent.Args{"file": level, "prefab": "prefabs/ground.aeprefab", "name": "Ground2"})
|
||||||
second := ins.Created[0]
|
second := ins.Created[0]
|
||||||
e.must("scene.set", agent.Args{"file": level, "node": "Ground", "path": "SpriteRenderer.tint", "value": "#123456ff"})
|
e.must("scene.set", agent.Args{"file": level, "node": "GroundLeft", "path": "SpriteRenderer.tint", "value": "#123456ff"})
|
||||||
e.must("prefab.apply", agent.Args{"file": level, "node": "Ground", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
e.must("prefab.apply", agent.Args{"file": level, "node": "GroundLeft", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
||||||
if got := e.must("scene.get", agent.Args{"file": level, "node": second, "path": "SpriteRenderer.tint"}).Text; got != "#123456ff" {
|
if got := e.must("scene.get", agent.Args{"file": level, "node": second, "path": "SpriteRenderer.tint"}).Text; got != "#123456ff" {
|
||||||
t.Fatalf("second instance tint %q", got)
|
t.Fatalf("second instance tint %q", got)
|
||||||
}
|
}
|
||||||
again := e.must("prefab.apply", agent.Args{"file": level, "node": "Ground", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
again := e.must("prefab.apply", agent.Args{"file": level, "node": "GroundLeft", "level": "property", "owner": "engine.SpriteRenderer", "field": "tint"})
|
||||||
if len(again.Notes) != 1 || again.Notes[0] != "nothing to apply" {
|
if len(again.Notes) != 1 || again.Notes[0] != "nothing to apply" {
|
||||||
t.Fatalf("second apply %+v", again)
|
t.Fatalf("second apply %+v", again)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,11 +270,6 @@
|
|||||||
"owner": "save",
|
"owner": "save",
|
||||||
"doc": "Persist saves the node's transform and enabled flag in save games."
|
"doc": "Persist saves the node's transform and enabled flag in save games."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "game.Health",
|
|
||||||
"kind": "component",
|
|
||||||
"owner": "mygame"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "audio.AudioEvent",
|
"name": "audio.AudioEvent",
|
||||||
"kind": "data",
|
"kind": "data",
|
||||||
|
|||||||
@@ -11,12 +11,6 @@ import (
|
|||||||
"aego/sdk"
|
"aego/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type health struct {
|
|
||||||
Max float32 `aego:"max,min=1"`
|
|
||||||
Current float32 `aego:"current,min=0"`
|
|
||||||
Hits int64 `aego:"hits"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type enemyStats struct {
|
type enemyStats struct {
|
||||||
Speed float32 `aego:"speed,min=0,max=10"`
|
Speed float32 `aego:"speed,min=0,max=10"`
|
||||||
Cooldown float32 `aego:"cooldown,min=0,max=5"`
|
Cooldown float32 `aego:"cooldown,min=0,max=5"`
|
||||||
@@ -31,24 +25,10 @@ func (Platformer) Info() sdk.ModuleInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (Platformer) Register(r *sdk.Registry) error {
|
func (Platformer) Register(r *sdk.Registry) error {
|
||||||
if err := sdk.Component[health](r, sdk.Name("game.Health"), sdk.Persist("current", "hits")).Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := sdk.DataType[enemyStats](r, sdk.Name("mygame.EnemyStats")).Err(); err != nil {
|
if err := sdk.DataType[enemyStats](r, sdk.Name("mygame.EnemyStats")).Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m := r.ScriptModule("mygame/combat")
|
sdk.Live[enemyStats](r, "speed")
|
||||||
sdk.Proc2(m, "Damage", func(c *sdk.ScriptCtx, target sdk.NodeHandle, amount int64) {
|
|
||||||
if h, ok := sdk.Get[health](c.Scene, target); ok {
|
|
||||||
h.Current -= float32(amount)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sdk.Func1(m, "HealthOf", func(c *sdk.ScriptCtx, target sdk.NodeHandle) float32 {
|
|
||||||
if h, ok := sdk.Get[health](c.Scene, target); ok {
|
|
||||||
return h.Current
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ func TestScriptVectorDefaultPublished(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("PlayerController is not published")
|
t.Fatal("PlayerController is not published")
|
||||||
}
|
}
|
||||||
fd, ok := td.Field("JumpImpulse")
|
fd, ok := td.Field("Bounce")
|
||||||
if !ok || !fd.HasDefault || fd.Default.V != (mathx.Vec2{Y: -5}) {
|
if !ok || !fd.HasDefault || fd.Default.V != (mathx.Vec2{Y: -9}) {
|
||||||
t.Fatalf("JumpImpulse default %+v", fd)
|
t.Fatalf("Bounce default %+v", fd)
|
||||||
}
|
}
|
||||||
if v := pf.field("JumpImpulse"); v.Lane(1) != -5 {
|
if v := pf.field("Bounce"); v.Lane(1) != -9 {
|
||||||
t.Fatalf("instance JumpImpulse lane y = %v", v.Lane(1))
|
t.Fatalf("instance Bounce lane y = %v", v.Lane(1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func TestExamplesCanonical(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"platformer", []sdk.Module{combatModule{}}},
|
{"platformer", []sdk.Module{combatModule{}}},
|
||||||
{"physics-sandbox", nil},
|
{"physics-sandbox", nil},
|
||||||
|
{"topdown", nil},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.dir, func(t *testing.T) {
|
t.Run(tc.dir, func(t *testing.T) {
|
||||||
canonicalExample(t, filepath.Join("..", "..", "examples", tc.dir), tc.mods)
|
canonicalExample(t, filepath.Join("..", "..", "examples", tc.dir), tc.mods)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"aego/contracts/debugdraw"
|
"aego/contracts/debugdraw"
|
||||||
"aego/contracts/persistence"
|
"aego/contracts/persistence"
|
||||||
"aego/modules/debug"
|
"aego/modules/debug"
|
||||||
|
"aego/modules/gameplay"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
"aego/save"
|
"aego/save"
|
||||||
"aego/script/runtime"
|
"aego/script/runtime"
|
||||||
@@ -249,7 +250,7 @@ func TestKeyframeSeekInsideCoroutineWait(t *testing.T) {
|
|||||||
for i := 0; i < 240; i++ {
|
for i := 0; i < 240; i++ {
|
||||||
pc.Tick(1.0 / 60)
|
pc.Tick(1.0 / 60)
|
||||||
hashes[i+1] = pausedHash(pc)
|
hashes[i+1] = pausedHash(pc)
|
||||||
if h, ok := scene.Get[Health](pc.Scene(), pc.Scene().Find(pc.Scene().Root(), "Player")); ok && h.Hits > 0 && hit == 0 {
|
if h, ok := scene.Get[gameplay.Health](pc.Scene(), pc.Scene().Find(pc.Scene().Root(), "Player")); ok && h.Current < h.Max && hit == 0 {
|
||||||
hit = i + 1
|
hit = i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,8 +312,8 @@ func TestPlatformerEnemyPrefabs(t *testing.T) {
|
|||||||
if !ok || !pi.Owner || s.Local(h).Position.X != x {
|
if !ok || !pi.Owner || s.Local(h).Position.X != x {
|
||||||
t.Fatalf("%s must be an instance at x=%v", name, x)
|
t.Fatalf("%s must be an instance at x=%v", name, x)
|
||||||
}
|
}
|
||||||
if !hasComponent(s, h, "physics.Body2D") || !hasComponent(s, h, "physics.PolygonCollider2D") {
|
if !hasComponent(s, h, "physics.Body2D") || !hasComponent(s, h, "gameplay.Hurtbox2D") || !hasComponent(s, h, "gameplay.Hitbox2D") {
|
||||||
t.Fatalf("%s lost its body or stomp trigger", name)
|
t.Fatalf("%s lost its body, hurtbox or hitbox", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out := string(sceneio.SaveWith(s, lib))
|
out := string(sceneio.SaveWith(s, lib))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"aego/app/edit"
|
"aego/app/edit"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"aego/core/rewind"
|
"aego/core/rewind"
|
||||||
"aego/editor/core/play"
|
"aego/editor/core/play"
|
||||||
"aego/modules/camera"
|
"aego/modules/camera"
|
||||||
|
inputmod "aego/modules/input"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
"aego/platform"
|
"aego/platform"
|
||||||
"aego/platform/input"
|
"aego/platform/input"
|
||||||
@@ -180,31 +182,33 @@ func cameraHash(t *testing.T, pc *play.Controller) [32]byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEditorRewindKeepsTheCamera(t *testing.T) {
|
func TestEditorRewindKeepsTheCamera(t *testing.T) {
|
||||||
s, pc := rewindSession(t)
|
_, pc := rewindSession(t)
|
||||||
in := s.Host.Engine.Input()
|
data, err := os.ReadFile(runFile)
|
||||||
hold := func(k input.Scancode, down bool) {
|
if err != nil {
|
||||||
kind := platform.EvKeyUp
|
t.Fatal(err)
|
||||||
if down {
|
|
||||||
kind = platform.EvKeyDown
|
|
||||||
}
|
|
||||||
platform.ApplyInput(in, platform.Event{Kind: kind, Key: input.KeyEvent{Scan: k}})
|
|
||||||
}
|
}
|
||||||
|
actions := inputmod.Of(pc.World())
|
||||||
|
if actions == nil {
|
||||||
|
t.Fatal("the play world has no input service")
|
||||||
|
}
|
||||||
|
if _, err := actions.Replay(data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const before, inside = 20, 330
|
||||||
marks := map[int][32]byte{}
|
marks := map[int][32]byte{}
|
||||||
for i := 0; i < 240; i++ {
|
for i := 1; i <= 360; i++ {
|
||||||
hold(input.KeyD, i >= 10 && i < 200)
|
|
||||||
hold(input.KeySpace, i >= 40 && i < 44)
|
|
||||||
pc.Tick(1.0 / 60)
|
pc.Tick(1.0 / 60)
|
||||||
if i+1 == 20 || i+1 == 120 {
|
if i == before || i == inside {
|
||||||
marks[i+1] = cameraHash(t, pc)
|
marks[i] = cameraHash(t, pc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if camera.Of(pc.World()).Active("").IsZero() {
|
if camera.Of(pc.World()).Active("").IsZero() {
|
||||||
t.Fatal("the platformer camera has no active virtual camera")
|
t.Fatal("the platformer camera has no active virtual camera")
|
||||||
}
|
}
|
||||||
if marks[20] == marks[120] {
|
if marks[before] == marks[inside] {
|
||||||
t.Fatal("the camera state is the same before and inside the arena zone")
|
t.Fatal("the camera state is the same before and inside the arena zone")
|
||||||
}
|
}
|
||||||
for _, f := range []int{120, 20, 120} {
|
for _, f := range []int{inside, before, inside} {
|
||||||
if err := pc.Seek(f); err != nil {
|
if err := pc.Seek(f); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,23 +31,25 @@ func TestComponentSignalReachesScriptAndGo(t *testing.T) {
|
|||||||
root := copyExample(t, "platformer")
|
root := copyExample(t, "platformer")
|
||||||
patch(t, filepath.Join(root, "scripts", "player", "player.aes"),
|
patch(t, filepath.Join(root, "scripts", "player", "player.aes"),
|
||||||
"\tvar blink Coroutine\n", "\tvar blink Coroutine\n\tvar hitConn Connection\n\tvar signalHits int\n",
|
"\tvar blink Coroutine\n", "\tvar blink Coroutine\n\tvar hitConn Connection\n\tvar signalHits int\n",
|
||||||
"\t\twatch(EnemyB)\n", "\t\twatch(EnemyB)\n\t\thitConn = node.Get<Health>().Hit.Connect(OnHit)\n",
|
"\t\twatch(EnemyB)\n", "\t\twatch(EnemyB)\n\t\thitConn = node.Get<Health>().Healed.Connect(OnHit)\n",
|
||||||
"\tfunc OnEnemyDied() {", "\tfunc OnHit(hits int) {\n\t\tsignalHits = hits\n\t}\n\n\tfunc OnEnemyDied() {")
|
"\tfunc OnEnemyDied() {", "\tfunc OnHit(hits int) {\n\t\tsignalHits = hits\n\t}\n\n\tfunc OnEnemyDied() {")
|
||||||
pf := platformerAt(t, root)
|
pf := platformerAt(t, root)
|
||||||
pf.step(1)
|
pf.step(1)
|
||||||
var goHits int64
|
var goHits int64
|
||||||
ctx := &sdk.Context{World: pf.app.Engine.MainWorld(), Engine: pf.app.Engine}
|
ctx := &sdk.Context{World: pf.app.Engine.MainWorld(), Engine: pf.app.Engine}
|
||||||
if _, err := sdk.Connect1[int64](ctx, pf.player, "Health.Hit", func(n int64) { goHits = n }); err != nil {
|
if _, err := sdk.Connect1[int64](ctx, pf.player, "Health.Healed", func(n int64) { goHits = n }); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pf.step(29)
|
pf.step(29)
|
||||||
pf.hitPlayer(t)
|
pf.hitPlayer(t)
|
||||||
hits := pf.health().Hits
|
pf.health().Heal(1)
|
||||||
|
pf.step(1)
|
||||||
|
hits := int64(1)
|
||||||
if got := pf.field("signalHits").Int(); got != hits {
|
if got := pf.field("signalHits").Int(); got != hits {
|
||||||
t.Fatalf("script handler saw %d hits, health has %d", got, hits)
|
t.Fatalf("script handler saw %d healed, want %d", got, hits)
|
||||||
}
|
}
|
||||||
if goHits != hits {
|
if goHits != hits {
|
||||||
t.Fatalf("Go handler saw %d hits, health has %d", goHits, hits)
|
t.Fatalf("Go handler saw %d healed, want %d", goHits, hits)
|
||||||
}
|
}
|
||||||
if !pf.app.Scripts.SignalConnected(pf.field("hitConn")) {
|
if !pf.app.Scripts.SignalConnected(pf.field("hitConn")) {
|
||||||
t.Fatal("the script connection must stay connected")
|
t.Fatal("the script connection must stay connected")
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
aeinput 1
|
||||||
|
project platformer
|
||||||
|
seed 0
|
||||||
|
fixedHz 60
|
||||||
|
players 1
|
||||||
|
actions Move Jump Attack Pause Back QuickSave QuickLoad ToggleSettings
|
||||||
|
frames 900
|
||||||
|
s 1 0 3 3f800000 0
|
||||||
|
s 31 1 3 3f800000 0
|
||||||
|
s 71 0 1 3f5ea880 0
|
||||||
|
s 71 1 4 0 0
|
||||||
|
s 72 0 1 3f384200 0
|
||||||
|
s 73 0 1 3f161fc0 0
|
||||||
|
s 74 0 4 3ec5d900 0
|
||||||
|
s 75 0 0 3df54200 0
|
||||||
|
s 76 0 0 be055f00 0
|
||||||
|
s 77 0 0 beba2680 0
|
||||||
|
s 78 0 3 bf13b040 0
|
||||||
|
s 79 0 1 bf452e80 0
|
||||||
|
s 80 0 1 bf718e00 0
|
||||||
|
s 81 0 1 bf800000 0
|
||||||
|
s 96 0 1 bf7f3440 0
|
||||||
|
s 97 0 1 bf58cdc0 0
|
||||||
|
s 98 0 1 bf3296c0 0
|
||||||
|
s 99 0 1 bf10a400 0
|
||||||
|
s 100 0 4 bee5eb00 0
|
||||||
|
s 101 0 0 beb31680 0
|
||||||
|
s 102 0 0 be88cb00 0
|
||||||
|
s 103 0 0 be4e1000 0
|
||||||
|
s 104 0 0 be1b9b00 0
|
||||||
|
s 105 0 0 bdf46e00 0
|
||||||
|
s 106 0 0 bdd3c800 0
|
||||||
|
s 107 0 0 bdd54400 0
|
||||||
|
s 108 0 3 3f800000 0
|
||||||
|
s 149 0 4 0 0
|
||||||
|
s 163 0 3 3f800000 0
|
||||||
|
s 163 1 3 3f800000 0
|
||||||
|
s 203 1 4 0 0
|
||||||
|
s 225 0 1 bf800000 0
|
||||||
|
s 245 0 1 3f800000 0
|
||||||
|
s 269 0 1 bf800000 0
|
||||||
|
s 280 0 1 3f800000 0
|
||||||
|
s 296 0 1 bf800000 0
|
||||||
|
s 307 0 1 3f800000 0
|
||||||
|
s 321 0 4 3e862f80 0
|
||||||
|
s 322 0 0 3df3e400 0
|
||||||
|
s 323 0 0 bbed4000 0
|
||||||
|
s 324 0 0 bdef6a00 0
|
||||||
|
s 325 0 0 be538500 0
|
||||||
|
s 326 0 0 be8d6d00 0
|
||||||
|
s 327 0 0 bea6da80 0
|
||||||
|
s 328 0 0 beb60a00 0
|
||||||
|
s 329 0 0 bebafc80 0
|
||||||
|
s 330 0 0 beb5b180 0
|
||||||
|
s 331 0 0 bea62900 0
|
||||||
|
s 332 0 0 be8c6300 0
|
||||||
|
s 333 0 0 be50bf00 0
|
||||||
|
s 334 0 0 bde87a00 0
|
||||||
|
s 335 0 0 bb500000 0
|
||||||
|
s 336 0 0 3dda6800 0
|
||||||
|
s 337 0 0 3e4c9800 0
|
||||||
|
s 338 0 0 3e8bc080 0
|
||||||
|
s 339 0 3 3f800000 0
|
||||||
|
s 391 0 4 0 0
|
||||||
@@ -151,7 +151,7 @@ func writeMeta(path, id, importer string, version int, s pipeline.Settings) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func encodeOgg(bin string, src []byte) ([]byte, error) {
|
func encodeOgg(bin string, src []byte) ([]byte, error) {
|
||||||
cmd := exec.Command(bin, "-hide_banner", "-loglevel", "error", "-f", "wav", "-i", "pipe:0", "-c:a", "libvorbis", "-q:a", "1", "-f", "ogg", "pipe:1")
|
cmd := exec.Command(bin, "-hide_banner", "-loglevel", "error", "-f", "wav", "-i", "pipe:0", "-c:a", "libvorbis", "-q:a", "3", "-f", "ogg", "pipe:1")
|
||||||
cmd.Stdin = bytes.NewReader(src)
|
cmd.Stdin = bytes.NewReader(src)
|
||||||
var out, stderr bytes.Buffer
|
var out, stderr bytes.Buffer
|
||||||
cmd.Stdout, cmd.Stderr = &out, &stderr
|
cmd.Stdout, cmd.Stderr = &out, &stderr
|
||||||
|
|||||||
@@ -113,15 +113,18 @@ func (s *song) drumBar(bar int) {
|
|||||||
s.snare(s.at(bar, b), 0.45+0.15*(b-3)*4)
|
s.snare(s.at(bar, b), 0.45+0.15*(b-3)*4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if bar%8 == 0 {
|
||||||
|
s.cymbal(s.at(bar, 0), 0.45, 0.9, 4500, 0.5)
|
||||||
|
}
|
||||||
for e := range 8 {
|
for e := range 8 {
|
||||||
v, decay := 0.55, 0.022
|
v, decay := 0.55, 0.035
|
||||||
if e%2 == 1 {
|
if e%2 == 1 {
|
||||||
v = 1
|
v = 1
|
||||||
}
|
}
|
||||||
if e == 7 && bar%2 == 1 {
|
if e == 7 && bar%2 == 1 {
|
||||||
decay = 0.11
|
v, decay = 0.9, 0.16
|
||||||
}
|
}
|
||||||
s.hat(s.at(bar, float64(e)*0.5), v, decay)
|
s.cymbal(s.at(bar, float64(e)*0.5), v, decay, 7000, 0.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,15 +160,34 @@ func (s *song) snare(at int, v float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *song) hat(at int, v, decay float64) {
|
var metal = [6]float64{205.3, 304.4, 369.6, 522.7, 540, 800}
|
||||||
n := int((decay*6 + 0.01) * rate)
|
|
||||||
p1, p2 := 0.0, 0.0
|
// cymbal is six detuned square waves and noise through a three-pole high-pass, like an 808's metal circuit.
|
||||||
|
func (s *song) cymbal(at int, v, decay, cutoff, wet float64) {
|
||||||
|
n := int((decay*5 + 0.01) * rate)
|
||||||
|
var ph [6]float64
|
||||||
|
a := onePole(cutoff)
|
||||||
|
l1, l2, l3 := 0.0, 0.0, 0.0
|
||||||
|
v *= 0.9 + 0.1*s.rnd.next()
|
||||||
for j := range n {
|
for j := range n {
|
||||||
t := float64(j) / rate
|
t := float64(j) / rate
|
||||||
x := s.rnd.next()
|
m := 0.0
|
||||||
h := x - 2*p1 + p2
|
for k, f := range metal {
|
||||||
p2, p1 = p1, x
|
dt := f / rate
|
||||||
s.add(s.drums, at+j, 0.045*v*h*math.Exp(-t/decay)*edge(j, n, rate/2000))
|
ph[k] += dt
|
||||||
|
ph[k] -= math.Floor(ph[k])
|
||||||
|
q := ph[k] + 0.5
|
||||||
|
m += blepSaw(ph[k], dt) - blepSaw(q-math.Floor(q), dt)
|
||||||
|
}
|
||||||
|
x := m/6 + 0.5*s.rnd.next()
|
||||||
|
l1 += a * (x - l1)
|
||||||
|
h1 := x - l1
|
||||||
|
l2 += a * (h1 - l2)
|
||||||
|
h2 := h1 - l2
|
||||||
|
l3 += a * (h2 - l3)
|
||||||
|
y := 2 * v * (h2 - l3) * math.Exp(-t/decay) * math.Min(t/0.0015, 1) * edge(j, n, rate/2000)
|
||||||
|
s.add(s.drums, at+j, y)
|
||||||
|
s.add(s.reverb, at+j, wet*y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.8 KiB |