From 109f47ff328fa9e3650cf85de582bc6106d7a2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Sm=C3=B3=C5=82ka?= Date: Sat, 12 Nov 2022 14:20:39 +0100 Subject: [PATCH] Add naive missile turret --- archetype/bullet.go | 52 +++++++++++++- archetype/enemy.go | 145 ++++++++++++++++++++++++++++++++++---- assets/assets.go | 25 +++++-- assets/levels/level01.tmx | 4 +- assets/levels/tiles.tsx | 6 +- component/follower.go | 15 ++++ component/health.go | 6 +- component/observer.go | 19 +++++ component/shooter.go | 4 +- scene/game.go | 17 +++++ system/events.go | 2 +- system/follower.go | 40 +++++++++++ system/observer.go | 20 +----- system/shooter.go | 8 ++- 14 files changed, 320 insertions(+), 43 deletions(-) create mode 100644 component/follower.go create mode 100644 system/follower.go diff --git a/archetype/bullet.go b/archetype/bullet.go index bc84a76..d28d306 100644 --- a/archetype/bullet.go +++ b/archetype/bullet.go @@ -1,17 +1,23 @@ package archetype import ( + "time" + "github.com/yohamta/donburi" "github.com/yohamta/donburi/features/math" "github.com/yohamta/donburi/features/transform" + "github.com/yohamta/donburi/filter" + "github.com/yohamta/donburi/query" "github.com/m110/airplanes/assets" "github.com/m110/airplanes/component" + "github.com/m110/airplanes/engine" ) const ( playerBulletSpeed = 10 enemyBulletSpeed = 4 + enemyMissileSpeed = 2 ) func NewPlayerBullet(w donburi.World, player *component.PlayerData, position math.Vec2) { @@ -113,7 +119,7 @@ func NewEnemyBullet(w donburi.World, position math.Vec2, rotation float64) { ), ) - image := assets.Rocket + image := assets.Bullet t := transform.Transform.Get(bullet) t.LocalPosition = position @@ -138,3 +144,47 @@ func NewEnemyBullet(w donburi.World, position math.Vec2, rotation float64) { Layer: component.CollisionLayerEnemyBullets, }) } + +func NewEnemyMissile(w donburi.World, position math.Vec2, rotation float64) { + missile := w.Entry( + w.Create( + component.Velocity, + transform.Transform, + component.Sprite, + component.Despawnable, + component.Collider, + component.Follower, + ), + ) + + image := assets.Missile + + t := transform.Transform.Get(missile) + t.LocalPosition = position + t.LocalRotation = rotation + + component.Velocity.SetValue(missile, component.VelocityData{ + Velocity: transform.Right(missile).MulScalar(enemyMissileSpeed), + }) + + component.Sprite.SetValue(missile, component.SpriteData{ + Image: image, + Layer: component.SpriteLayerAirUnits, + Pivot: component.SpritePivotCenter, + OriginalRotation: -90, + }) + + width, height := image.Size() + + component.Collider.SetValue(missile, component.ColliderData{ + Width: float64(width), + Height: float64(height), + Layer: component.CollisionLayerEnemyBullets, + }) + + component.Follower.SetValue(missile, component.FollowerData{ + Target: component.ClosestTarget(w, missile, query.NewQuery(filter.Contains(component.PlayerAirplane))), + FollowingSpeed: enemyMissileSpeed, + FollowingTimer: engine.NewTimer(3 * time.Second), + }) +} diff --git a/archetype/enemy.go b/archetype/enemy.go index 6ee1d63..fc507eb 100644 --- a/archetype/enemy.go +++ b/archetype/enemy.go @@ -83,11 +83,9 @@ func NewEnemyAirplane( }) } - component.Health.SetValue(airplane, component.HealthData{ - Health: 3, - DamageIndicatorTimer: engine.NewTimer(time.Millisecond * 100), - DamageIndicator: newDamageIndicator(w, airplane), - }) + health := component.Health.Get(airplane) + health.Health = 3 + health.DamageIndicator = newDamageIndicator(w, airplane) NewShadow(w, airplane) } @@ -111,7 +109,6 @@ func NewEnemyTank( ), ) - transform.Reset(tank) t := transform.Transform.Get(tank) t.LocalPosition = position t.LocalRotation = rotation @@ -146,11 +143,9 @@ func NewEnemyTank( }) } - component.Health.SetValue(tank, component.HealthData{ - Health: 5, - DamageIndicatorTimer: engine.NewTimer(time.Millisecond * 100), - DamageIndicator: newDamageIndicator(w, tank), - }) + health := component.Health.Get(tank) + health.Health = 5 + health.DamageIndicator = newDamageIndicator(w, tank) gun := w.Entry( w.Create( @@ -163,7 +158,6 @@ func NewEnemyTank( ) originalRotation := 90.0 - transform.Reset(gun) gunT := transform.Transform.Get(gun) gunT.LocalPosition = position gunT.LocalRotation = originalRotation + rotation @@ -180,13 +174,138 @@ func NewEnemyTank( }) component.Shooter.SetValue(gun, component.ShooterData{ - Type: component.ShooterTypeRocket, + Type: component.ShooterTypeBullet, ShootTimer: engine.NewTimer(time.Millisecond * 2500), }) transform.AppendChild(tank, gun, true) } +func NewEnemyTurretBeam( + w donburi.World, + position math.Vec2, + rotation float64, +) { + turret := newEnemyTurret(w, position, rotation) + + gun := w.Entry( + w.Create( + transform.Transform, + component.Sprite, + component.Despawnable, + component.Observer, + component.Shooter, + ), + ) + + originalRotation := 90.0 + gunT := transform.Transform.Get(gun) + gunT.LocalPosition = position + gunT.LocalRotation = originalRotation + rotation + + component.Sprite.SetValue(gun, component.SpriteData{ + Image: assets.TurretGunSingle, + Layer: component.SpriteLayerGroundGuns, + Pivot: component.SpritePivotCenter, + OriginalRotation: originalRotation, + }) + + component.Observer.SetValue(gun, component.ObserverData{ + LookFor: query.NewQuery(filter.Contains(component.PlayerAirplane)), + }) + + component.Shooter.SetValue(gun, component.ShooterData{ + Type: component.ShooterTypeBeam, + ShootTimer: engine.NewTimer(time.Millisecond * 5000), + }) + + transform.AppendChild(turret, gun, true) +} + +func NewEnemyTurretMissiles( + w donburi.World, + position math.Vec2, + rotation float64, +) { + turret := newEnemyTurret(w, position, rotation) + + gun := w.Entry( + w.Create( + transform.Transform, + component.Sprite, + component.Despawnable, + component.Observer, + component.Shooter, + ), + ) + + originalRotation := 90.0 + gunT := transform.Transform.Get(gun) + gunT.LocalPosition = position + gunT.LocalRotation = originalRotation + rotation + + component.Sprite.SetValue(gun, component.SpriteData{ + Image: assets.TurretGunDouble, + Layer: component.SpriteLayerGroundGuns, + Pivot: component.SpritePivotCenter, + OriginalRotation: originalRotation, + }) + + component.Observer.SetValue(gun, component.ObserverData{ + LookFor: query.NewQuery(filter.Contains(component.PlayerAirplane)), + }) + + component.Shooter.SetValue(gun, component.ShooterData{ + Type: component.ShooterTypeMissile, + ShootTimer: engine.NewTimer(time.Millisecond * 5000), + }) + + transform.AppendChild(turret, gun, true) +} + +func newEnemyTurret( + w donburi.World, + position math.Vec2, + rotation float64, +) *donburi.Entry { + turret := w.Entry( + w.Create( + transform.Transform, + component.Sprite, + component.AI, + component.Despawnable, + component.Collider, + component.Health, + ), + ) + + t := transform.Transform.Get(turret) + t.LocalPosition = position + t.LocalRotation = rotation + + image := assets.TurretBase + component.Sprite.SetValue(turret, component.SpriteData{ + Image: image, + Layer: component.SpriteLayerGroundUnits, + Pivot: component.SpritePivotCenter, + OriginalRotation: 0, + }) + + width, height := image.Size() + + component.Collider.SetValue(turret, component.ColliderData{ + Width: float64(width), + Height: float64(height), + Layer: component.CollisionLayerGroundEnemies, + }) + + health := component.Health.Get(turret) + health.Health = 5 + health.DamageIndicator = newDamageIndicator(w, turret) + + return turret +} + func newDamageIndicator(w donburi.World, parent *donburi.Entry) *component.SpriteData { indicator := w.Entry( w.Create( diff --git a/assets/assets.go b/assets/assets.go index 31e2e28..71adc99 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -40,8 +40,13 @@ var ( TankBase *ebiten.Image TankGun *ebiten.Image + TurretBase *ebiten.Image + TurretGunSingle *ebiten.Image + TurretGunDouble *ebiten.Image + LaserSingle *ebiten.Image - Rocket *ebiten.Image + Bullet *ebiten.Image + Missile *ebiten.Image Health *ebiten.Image WeaponUpgrade *ebiten.Image @@ -59,8 +64,10 @@ var ( ) const ( - EnemyClassAirplane = "enemy-airplane" - EnemyClassTank = "enemy-tank" + EnemyClassAirplane = "enemy-airplane" + EnemyClassTank = "enemy-tank" + EnemyClassTurretBeam = "enemy-turret-beam" + EnemyClassTurretMissiles = "enemy-turret-missiles" TilesetClassTiles = "tiles" TilesetClassAirplanes = "airplanes" @@ -129,8 +136,13 @@ func MustLoadAssets() { TankBase = loader.MustFindTile(TilesetClassTiles, "tank-base") TankGun = loader.MustFindTile(TilesetClassTiles, "tank-gun") + TurretBase = loader.MustFindTile(TilesetClassTiles, "turret-base") + TurretGunSingle = loader.MustFindTile(TilesetClassTiles, "turret-gun-single") + TurretGunDouble = loader.MustFindTile(TilesetClassTiles, "turret-gun-double") + LaserSingle = loader.MustFindTile(TilesetClassTiles, "laser-single") - Rocket = loader.MustFindTile(TilesetClassTiles, "rocket") + Bullet = loader.MustFindTile(TilesetClassTiles, "bullet") + Missile = loader.MustFindTile(TilesetClassTiles, "missile") Health = loader.MustFindTile(TilesetClassTiles, "health") WeaponUpgrade = loader.MustFindTile(TilesetClassTiles, "weapon-upgrade") @@ -237,7 +249,10 @@ func (l *levelLoader) MustLoadLevel(levelPath string) Level { for _, og := range levelMap.ObjectGroups { for _, o := range og.Objects { - if o.Class == EnemyClassAirplane || o.Class == EnemyClassTank { + if o.Class == EnemyClassAirplane || + o.Class == EnemyClassTank || + o.Class == EnemyClassTurretBeam || + o.Class == EnemyClassTurretMissiles { enemy := Enemy{ Class: o.Class, Position: math.Vec2{ diff --git a/assets/levels/level01.tmx b/assets/levels/level01.tmx index 8e4e9a0..c42a7ef 100644 --- a/assets/levels/level01.tmx +++ b/assets/levels/level01.tmx @@ -1,5 +1,5 @@ - + @@ -168,6 +168,8 @@ + + diff --git a/assets/levels/tiles.tsx b/assets/levels/tiles.tsx index 685e461..3e5d407 100644 --- a/assets/levels/tiles.tsx +++ b/assets/levels/tiles.tsx @@ -2,7 +2,11 @@ - + + + + + diff --git a/component/follower.go b/component/follower.go new file mode 100644 index 0000000..b9e71a2 --- /dev/null +++ b/component/follower.go @@ -0,0 +1,15 @@ +package component + +import ( + "github.com/yohamta/donburi" + + "github.com/m110/airplanes/engine" +) + +type FollowerData struct { + Target *donburi.Entry + FollowingSpeed float64 + FollowingTimer *engine.Timer +} + +var Follower = donburi.NewComponentType[FollowerData]() diff --git a/component/health.go b/component/health.go index d3f448e..e5d723a 100644 --- a/component/health.go +++ b/component/health.go @@ -1,6 +1,8 @@ package component import ( + "time" + "github.com/yohamta/donburi" "github.com/m110/airplanes/engine" @@ -29,4 +31,6 @@ func (d *HealthData) HideDamageIndicator() { d.DamageIndicator.Hidden = true } -var Health = donburi.NewComponentType[HealthData]() +var Health = donburi.NewComponentType[HealthData](HealthData{ + DamageIndicatorTimer: engine.NewTimer(time.Millisecond * 100), +}) diff --git a/component/observer.go b/component/observer.go index 6867dda..bb74c23 100644 --- a/component/observer.go +++ b/component/observer.go @@ -2,6 +2,7 @@ package component import ( "github.com/yohamta/donburi" + "github.com/yohamta/donburi/features/transform" "github.com/yohamta/donburi/query" ) @@ -11,3 +12,21 @@ type ObserverData struct { } var Observer = donburi.NewComponentType[ObserverData]() + +func ClosestTarget(w donburi.World, entry *donburi.Entry, lookFor *query.Query) *donburi.Entry { + pos := transform.WorldPosition(entry) + + var closestDistance float64 + var closestTarget *donburi.Entry + lookFor.EachEntity(w, func(target *donburi.Entry) { + targetPos := transform.WorldPosition(target) + distance := pos.Distance(targetPos) + + if closestTarget == nil || distance < closestDistance { + closestTarget = target + closestDistance = distance + } + }) + + return closestTarget +} diff --git a/component/shooter.go b/component/shooter.go index b3b3a56..6df22f2 100644 --- a/component/shooter.go +++ b/component/shooter.go @@ -9,8 +9,8 @@ import ( type ShooterType int const ( - ShooterTypeRocket ShooterType = iota - ShooterTypeMissiles + ShooterTypeBullet ShooterType = iota + ShooterTypeMissile ShooterTypeBeam ) diff --git a/scene/game.go b/scene/game.go index eeb5fe2..08efae9 100644 --- a/scene/game.go +++ b/scene/game.go @@ -82,6 +82,7 @@ func (g *Game) loadLevel() { system.NewEvolution(), system.NewAltitude(), system.NewEvents(), + system.NewFollower(), render, debug, } @@ -144,6 +145,22 @@ func (g *Game) createWorld(levelIndex int) donburi.World { enemy.Path, ) }) + case assets.EnemyClassTurretBeam: + archetype.NewEnemySpawn(world, pos, func(w donburi.World) { + archetype.NewEnemyTurretBeam( + w, + enemy.Position, + enemy.Rotation, + ) + }) + case assets.EnemyClassTurretMissiles: + archetype.NewEnemySpawn(world, pos, func(w donburi.World) { + archetype.NewEnemyTurretMissiles( + w, + enemy.Position, + enemy.Rotation, + ) + }) default: panic("unknown enemy class: " + enemy.Class) } diff --git a/system/events.go b/system/events.go index 3ba3103..1ee7024 100644 --- a/system/events.go +++ b/system/events.go @@ -19,7 +19,7 @@ type EnemyKilled struct { var EnemyKilledEvent = events.NewEventType[EnemyKilled]() func OnEnemyKilledWreck(w donburi.World, event EnemyKilled) { - if event.Enemy.HasComponent(component.Wreckable) { + if event.Enemy.Valid() && event.Enemy.HasComponent(component.Wreckable) { archetype.NewAirplaneWreck(w, event.Enemy, component.Sprite.Get(event.Enemy)) } } diff --git a/system/follower.go b/system/follower.go new file mode 100644 index 0000000..a42f059 --- /dev/null +++ b/system/follower.go @@ -0,0 +1,40 @@ +package system + +import ( + "github.com/yohamta/donburi" + "github.com/yohamta/donburi/features/transform" + "github.com/yohamta/donburi/filter" + "github.com/yohamta/donburi/query" + + "github.com/m110/airplanes/component" +) + +type Follower struct { + query *query.Query +} + +func NewFollower() *Follower { + return &Follower{ + query: query.NewQuery(filter.Contains(transform.Transform, component.Follower)), + } +} + +func (s *Follower) Update(w donburi.World) { + s.query.EachEntity(w, func(entry *donburi.Entry) { + follower := component.Follower.Get(entry) + if follower.Target == nil || !follower.Target.Valid() { + return + } + + follower.FollowingTimer.Update() + if follower.FollowingTimer.IsReady() { + follower.Target = nil + return + } + + // TODO: Should rather rotate towards the target instead of looking at it straight away. + targetPos := transform.WorldPosition(follower.Target) + transform.LookAt(entry, targetPos) + component.Velocity.Get(entry).Velocity = transform.Right(entry).MulScalar(follower.FollowingSpeed) + }) +} diff --git a/system/observer.go b/system/observer.go index ffa4792..bdfee94 100644 --- a/system/observer.go +++ b/system/observer.go @@ -26,27 +26,13 @@ func (s *Observer) Update(w donburi.World) { return } - pos := transform.WorldPosition(entry) - - var closestDistance float64 - var closestTarget *donburi.Entry - observer.LookFor.EachEntity(w, func(target *donburi.Entry) { - targetPos := transform.WorldPosition(target) - distance := pos.Distance(targetPos) - - if closestTarget == nil || distance < closestDistance { - closestTarget = target - closestDistance = distance - } - }) - - observer.Target = closestTarget - if closestTarget == nil { + observer.Target = component.ClosestTarget(w, entry, observer.LookFor) + if observer.Target == nil { return } // TODO: Should rather rotate towards the target instead of looking at it straight away. - targetPos := transform.WorldPosition(closestTarget) + targetPos := transform.WorldPosition(observer.Target) transform.LookAt(entry, targetPos) }) } diff --git a/system/shooter.go b/system/shooter.go index 5bd6d4e..00124bf 100644 --- a/system/shooter.go +++ b/system/shooter.go @@ -40,7 +40,13 @@ func (s *Shooter) Update(w donburi.World) { if shooter.ShootTimer.IsReady() { shooter.ShootTimer.Reset() - archetype.NewEnemyBullet(w, transform.WorldPosition(entry), transform.WorldRotation(entry)) + switch shooter.Type { + case component.ShooterTypeBullet: + archetype.NewEnemyBullet(w, transform.WorldPosition(entry), transform.WorldRotation(entry)) + case component.ShooterTypeMissile: + archetype.NewEnemyMissile(w, transform.WorldPosition(entry), transform.WorldRotation(entry)) + case component.ShooterTypeBeam: + } } }) }