3d/main.go
2025-10-14 15:04:32 -03:00

1014 lines
21 KiB
Go

package main
import (
"fmt"
"math"
"math/rand"
rl "github.com/gen2brain/raylib-go/raylib"
)
const (
blocksNumber = 10
buildsPerBlock = 10
totalBuildings = 1024
// totalBuildings = 1
MARGIN = 20
WIDTH = 1100
HEIGHT = 700
buildingSpacing = 18
buildingSize = 8
maxBuildingHeight = 60
maxColumns = 5
enemyAcc = 0.01
maxSpeed = 0.1
buildingsHealth = 20
oneSecond second = 1
)
var gravity = rl.NewVector3(0, -2, 0)
// var globalParticles []Particle
type object struct {
pos rl.Vector3
}
func (b *object) Pos() rl.Vector3 { return b.pos }
type livingObject struct {
object
health int
}
func (o *livingObject) IsAlive() bool { return o.health <= 0 }
func (o *livingObject) Life() int { return o.health }
func (o *livingObject) CauseDamage(damage int) { o.health -= damage }
type kinecticObject struct {
object
speed rl.Vector3
}
type shootingEnemy struct {
livingObject
dodgeDelay float64
animate animationFunc
color rl.Color
fireRate *timer
}
type projectile struct {
kinecticObject
destroyed bool
}
type bombProjectile struct {
projectile
}
type missileProjectile struct {
projectile
}
type animationOver bool
type animationFunc func() animationOver
type building struct {
object
size rl.Vector3
color rl.Color
life int
boundingBox rl.BoundingBox
animate animationFunc
}
type healthBar struct {
life int
damageTaken float32
lastUpdate float64
pos Somewhere
}
type character struct {
camera rl.Camera
damage int
life int
}
type scene struct {
things []DrawableLivingThing
// buildings []Building
healthBar *healthBar
// projectiles []*projectile
character Player
particles []Particle
// enemies []Enemy
// emitters []Emitter
}
func (s *scene) AppendThing(t ...DrawableLivingThing) { s.things = append(s.things, t...) }
func (s *scene) AppendParticle(p ...Particle) { s.particles = append(s.particles, p...) }
func GetThing[T DrawableLivingThing](s *scene) []T {
var ret []T
for _, thing := range s.things {
if conv, ok := thing.(T); ok && conv.IsAlive() {
ret = append(ret, conv)
}
}
return ret
}
func (s *scene) Buildings() []Building {
b := GetThing[*building](s)
var r []Building = make([]Building, len(b))
for i := range b {
r[i] = b[i]
}
return r
}
func (s *scene) Enemies() []Enemy {
return GetThing[Enemy](s)
}
type second float64
type timer struct {
ttl second
time float32
start float64
}
func (t *timer) tick() {
t.time += rl.GetFrameTime()
}
// tick, timeout, reset
func (t *timer) ttr() bool {
t.tick()
if t.isTimeout() {
t.reset()
return true
}
return false
}
func (t *timer) isTimeout() bool {
return t.time >= float32(t.ttl)
}
func (t *timer) reset() {
t.start = float64(t.time)
t.time = 0
}
func (t *timer) unit() float32 {
return t.time / float32(t.ttl)
}
func newTimer(duration second) *timer {
return &timer{time: 0, ttl: duration, start: rl.GetTime()}
}
func newTimerWithStartDelay(duration, delay second) *timer {
return &timer{time: float32(delay), ttl: duration, start: rl.GetTime()}
}
type Thing interface {
Update()
}
type Somewhere interface {
Pos() rl.Vector3
}
type Drawable interface {
Draw()
}
type Living interface {
IsAlive() bool
Life() int
CauseDamage(int)
}
type DrawableLivingThing interface {
Collide
Drawable
Living
Thing
Somewhere
}
type Animate interface {
Animation() animationFunc
Animate() animationOver
SetAnimation(animationFunc)
}
type Collide interface {
RayCollision(rl.Ray) rl.RayCollision
SphereCollision(centerSphere rl.Vector3, radiusSphere float32) bool
}
type Particle interface {
Thing
Living
Drawable
}
type Enemy interface {
Collide
Thing
Drawable
Living
Somewhere
Animate
}
type Player interface {
Somewhere
Thing
// Drawable
Living
}
type Building interface {
Somewhere
Thing
Drawable
Living
Animate
Collide
}
// type Emitter interface {
// Alive
// Drawable
// Update() int
// }
type simpleEmitter struct {
particles []Particle
pos rl.Vector3
particleAmount int
}
type explosionEmitter struct {
simpleEmitter
}
type simpleParticle struct {
// birth float32
lifetime float64
pos rl.Vector3
speed rl.Vector3
}
type explosionParticle struct {
simpleParticle
trailTimer *timer
}
type smokeTrailParticle struct {
simpleParticle
}
var city *scene
func NewHealthBar(where Somewhere) *healthBar {
return &healthBar{pos: where}
}
func NewShootingEnemy(pos rl.Vector3, health int) Enemy {
return &shootingEnemy{
dodgeDelay: rand.Float64() * 3.1,
fireRate: newTimerWithStartDelay(0.5, second(rand.Float64()*10.1)),
livingObject: livingObject{object: object{pos: pos}, health: health},
}
}
func (e *shootingEnemy) RayCollision(ray rl.Ray) rl.RayCollision {
return rl.GetRayCollisionSphere(ray, e.pos, 1)
}
func (e *shootingEnemy) SphereCollision(centerSphere rl.Vector3, radiusSphere float32) bool {
return rl.CheckCollisionSpheres(e.pos, 1, centerSphere, radiusSphere)
}
func (e *shootingEnemy) Animate() animationOver {
if e.animate != nil {
return e.animate()
} else {
return true
}
}
func (e *shootingEnemy) Animation() animationFunc {
return e.animate
}
func (e *shootingEnemy) SetAnimation(f animationFunc) {
e.animate = f
}
func (e *shootingEnemy) CauseDamage(damage int) {
e.health -= damage
e.animate = shakeEnemy(e)
}
func (e *shootingEnemy) IsAlive() bool {
return e.health > 0 || e.animate != nil
}
func (e *shootingEnemy) Draw() {
if e.Animation() != nil {
if over := e.Animate(); over {
e.SetAnimation(nil)
}
return
}
playerDirection := rl.NewVector3(0, 1, 0)
rl.DrawSphere(e.Pos(), 1, rl.Red)
rl.DrawLine3D(e.pos, rl.Vector3Add(e.pos, rl.Vector3Scale(playerDirection, 40)), rl.Blue)
}
func raycast(pos, direction rl.Vector3) (DrawableLivingThing, bool) {
var (
closestDistance = math.Inf(1)
closest DrawableLivingThing
)
for _, thing := range city.things {
if !thing.IsAlive() {
continue
}
thingDistance := rl.Vector3Distance(pos, thing.Pos())
if thingDistance > float32(closestDistance) {
continue
}
collision := thing.RayCollision(rl.NewRay(pos, direction))
if collision.Hit {
closestDistance = float64(thingDistance)
closest = thing
}
}
return closest, closest != nil
}
func (e *shootingEnemy) Update() {
if e.health <= 0 {
return
}
e.fireRate.tick()
playerDistance := rl.Vector3Subtract(city.character.Pos(), e.Pos())
playerDirection := rl.Vector3Normalize(playerDistance)
if hitThing, ok := raycast(e.pos, playerDirection); ok {
if _, ok := hitThing.(*character); ok && e.fireRate.isTimeout() {
spawnPos := rl.Vector3Add(e.Pos(), rl.Vector3Scale(playerDirection, 2))
speed := rl.Vector3Scale(playerDirection, 1.5)
city.AppendParticle(NewMissileProjectile(spawnPos, speed))
e.fireRate.reset()
}
}
var speed, sideways rl.Vector3
if rl.Vector3Length(playerDistance) < 50 {
distance := rl.Vector3Negate(playerDirection)
speed = rl.Vector3Add(speed, distance)
}
{
sideways = rl.Vector3Transform(playerDirection, rl.MatrixRotateY(90))
sideways = rl.Vector3Scale(sideways, float32(math.Sin(rl.GetTime()+e.dodgeDelay)))
speed = rl.Vector3Add(speed, sideways)
}
speed = rl.Vector3Normalize(speed)
e.pos = rl.Vector3Add(e.pos, rl.Vector3Scale(speed, 0.1))
}
func (m *missileProjectile) CauseDamage(int) {
}
func (m *missileProjectile) Update() {
m.pos = rl.Vector3Add(m.pos, m.speed)
var (
closestDistance = math.Inf(1)
closest Building
)
for _, building := range city.Buildings() {
if !building.IsAlive() {
continue
}
hitDistance := rl.Vector3Distance(m.Pos(), building.Pos())
if hitDistance < float32(closestDistance) {
if building.SphereCollision(m.Pos(), 0.5) {
closest = building
}
}
}
if closest != nil {
closest.CauseDamage(3)
m.destroyed = true
}
}
func (m *missileProjectile) Life() int {
return 999
}
func (m *missileProjectile) IsAlive() bool {
return !m.destroyed
}
func (m *missileProjectile) Draw() {
n := rl.Vector3Normalize(m.speed)
rl.PushMatrix()
{
rl.Translatef(m.pos.X, m.pos.Y, m.pos.Z)
rl.Rotatef(270, n.X, n.Y, n.Z)
rl.DrawCylinder(rl.Vector3Zero(), 0.1, 0.2, 0.5, 8, rl.White)
}
rl.PopMatrix()
}
func (particle *simpleParticle) IsAlive() bool {
return rl.GetTime() < particle.lifetime
}
func (p *simpleParticle) Update() {
p.pos = rl.Vector3Add(p.pos, p.speed)
}
func (p *simpleParticle) Pos() rl.Vector3 {
return p.pos
}
func (particle *simpleParticle) Draw() {
panic("não implementado")
}
func NewExplosionParticle(direction, pos rl.Vector3) *explosionParticle {
return &explosionParticle{
simpleParticle: simpleParticle{
lifetime: rl.GetTime() + 10 + rand.Float64()*5, speed: direction, pos: pos,
},
trailTimer: newTimer(oneSecond * 0.01),
}
}
func (p *explosionParticle) CauseDamage(int) {}
func (p *explosionParticle) Life() int { return -1 }
func (p *explosionParticle) Update() {
timer := p.trailTimer
timer.tick()
p.speed = rl.Vector3Add(p.speed, gravity)
newPos := rl.Vector3Add(p.pos, rl.Vector3Scale(p.speed, rl.GetFrameTime()))
if newPos.Y <= 0 {
p.pos.Y = 0
} else {
p.pos = newPos
if timer.isTimeout() {
// addParticles(NewSmokeTrailParticle(p.pos, p.speed))
timer.reset()
}
}
}
func (p *explosionParticle) Draw() {
rl.DrawCubeV(p.pos, rl.NewVector3(.1, .1, .1), rl.Gray)
}
// func NewSmokeTrailParticle(pos, speed rl.Vector3) *smokeTrailParticle {
// return &smokeTrailParticle{
// simpleParticle: simpleParticle{
// lifetime: rl.GetTime() + 1,
// speed: rl.Vector3Add(rl.NewVector3(0, .1, 0), rl.Vector3Scale(speed, 0.1)), pos: pos,
// },
// }
// }
func (p *smokeTrailParticle) Draw() {
rl.BeginBlendMode(rl.BlendAdditive)
{
rl.DrawCubeV(p.pos, rl.NewVector3(.05, .05, .05), rl.Gray)
}
rl.EndBlendMode()
}
func (p *bombProjectile) Pos() rl.Vector3 {
return p.pos
}
func (p *bombProjectile) Draw() {
rl.DrawSphere(p.pos, 0.5, rl.Gray)
}
func (p *bombProjectile) IsAlive() bool {
return !p.destroyed
}
func (p *bombProjectile) CauseDamage(int) {
p.destroyed = true
}
func (p *bombProjectile) Life() int {
return 1
}
func (p *bombProjectile) Update() {
p.speed = rl.Vector3Add(p.speed, gravity)
// FIXME: Voa mais do que deveria em frame rates mais baixos.
p.pos = rl.Vector3Add(p.pos, rl.Vector3Scale(p.speed, rl.GetFrameTime()))
if rl.Vector3Length(p.pos) > 1000 {
p.destroyed = true
}
explode := func() {
p.destroyed = true
for range 500 {
direction := rl.NewVector3(rand.Float32()*2-1, 1, rand.Float32()*2-1)
direction = rl.Vector3Normalize(direction)
direction = rl.Vector3Scale(direction, 35+rand.Float32()*35)
city.AppendParticle(NewExplosionParticle(direction, p.pos))
}
for _, thing := range city.things {
if dist := rl.Vector3Distance(p.pos, thing.Pos()); dist < 40 {
thing.CauseDamage(10)
}
}
}
if p.pos.Y < 0 {
explode()
return
}
var (
closestDistance = math.Inf(1)
closest Building
)
for _, building := range city.Buildings() {
if !building.IsAlive() {
continue
}
hitDistance := rl.Vector3Distance(p.pos, building.Pos())
if hitDistance < float32(closestDistance) {
if building.SphereCollision(p.pos, 0.5) {
closest = building
}
}
}
if closest != nil {
explode()
return
}
}
func drawPlane() {
rl.DrawPlane(
rl.NewVector3(16, 0.0, 16),
rl.NewVector2(2048, 2048),
rl.NewColor(50, 50, 50, 255),
)
}
func (s *scene) Draw() {
character, ok := s.character.(*character)
if !ok {
panic(ok)
}
rl.BeginDrawing()
rl.ClearBackground(rl.LightGray)
rl.BeginMode3D(character.camera)
{
for _, thing := range s.things {
// FIXME:
if drawable, ok := thing.(Drawable); ok {
drawable.Draw()
}
}
for _, particle := range s.particles {
particle.Draw()
}
drawPlane()
// direction = rl.Vector3Subtract(
// character.camera.Target, character.camera.Position)
// aim = rl.Vector3Lerp(direction, aim, 0.95)
// rl.DrawSphere(rl.Vector3Add(character.camera.Position, aim), 0.1, rl.Red)
}
rl.EndMode3D()
drawHUD()
rl.DrawLineV(
rl.NewVector2(WIDTH/2-5, HEIGHT/2-5),
rl.NewVector2(WIDTH/2+5, HEIGHT/2+5),
rl.Red,
)
rl.DrawLineV(
rl.NewVector2(WIDTH/2-5, HEIGHT/2+5),
rl.NewVector2(WIDTH/2+5, HEIGHT/2-5),
rl.Red,
)
rl.DrawFPS(0, HEIGHT-30)
rl.EndDrawing()
}
func (s *scene) Update() {
for i := 0; i < len(s.particles); i++ {
particle := s.particles[i]
if !particle.IsAlive() {
city.particles[i] = city.particles[len(city.particles)-1]
city.particles = city.particles[:len(city.particles)-1]
i--
continue
}
particle.Update()
}
for i := 0; i < len(s.things); i++ {
thing := s.things[i]
if alive, ok := thing.(Living); ok && !alive.IsAlive() {
city.things[i] = city.things[len(city.things)-1]
city.things = city.things[:len(city.things)-1]
i--
continue
}
thing.Update()
}
}
func (c *character) Draw() {
}
func (c *character) Life() int {
return c.life
}
func (c *character) RayCollision(rl.Ray) rl.RayCollision {
return rl.RayCollision{}
}
func (c *character) SphereCollision(centerSphere rl.Vector3, radiusSphere float32) bool {
return false
}
func (c *character) CauseDamage(damage int) {
c.life -= damage
}
func (c *character) IsAlive() bool {
return c.life <= 0
}
func (c *character) Pos() rl.Vector3 {
return c.camera.Position
}
func (c *character) Update() {
shake := rl.NewVector3(
randomShake(0.003)*city.healthBar.damageTaken,
randomShake(0.003)*city.healthBar.damageTaken,
randomShake(0.003)*city.healthBar.damageTaken,
)
c.camera.Target = rl.Vector3Add(c.camera.Target, shake)
rl.UpdateCamera(&c.camera, rl.CameraFirstPerson)
// c.laserBeam()
// c.killAurea()
c.throwBomb()
}
func (c *character) killAurea() {
const min = 10
for _, b := range city.Buildings() {
if distance := rl.Vector3Distance(c.camera.Position, b.Pos()); distance < min {
b.CauseDamage(c.damage)
}
}
}
func NewProjectile(pos, direction rl.Vector3) *projectile {
return &projectile{kinecticObject: kinecticObject{object: object{pos: pos}, speed: direction}}
}
func NewBombProjectile(pos, direction rl.Vector3) *bombProjectile {
return &bombProjectile{projectile: *NewProjectile(pos, direction)}
}
func NewMissileProjectile(pos, direction rl.Vector3) *missileProjectile {
return &missileProjectile{projectile: *NewProjectile(pos, direction)}
}
func (c *character) throwBomb() {
if !rl.IsMouseButtonPressed(rl.MouseRightButton) {
return
}
direction := rl.Vector3Normalize(rl.Vector3Subtract(c.camera.Target, c.camera.Position))
city.AppendParticle(NewBombProjectile(c.camera.Position, rl.Vector3Scale(direction, 130)))
}
func newCharacter() *character {
return &character{
camera: rl.Camera3D{
Position: rl.NewVector3(4.0, 2.0, 4.0),
Target: rl.NewVector3(0.0, 1.8, 0.0),
Up: rl.NewVector3(0.0, 1.0, 0.0),
Fovy: 80.0,
Projection: rl.CameraPerspective,
},
damage: 1,
}
}
func randomShake(amount float32) float32 {
return rand.Float32()*amount - amount/2
}
func (hb *healthBar) Draw() {
const BARWIDTH = (WIDTH - MARGIN*2)
currentLife := 0
for _, thing := range city.things {
if building, ok := thing.(*building); ok {
currentLife += building.Life()
}
}
maxLife := buildingsHealth * totalBuildings
position := rl.NewVector2(MARGIN, 10)
barWidth := BARWIDTH * currentLife / maxLife
size := rl.NewVector2(float32(barWidth), 10)
hb.damageTaken += float32(hb.life - currentLife)
damageTakenBar := size
damageTakenBar.X = BARWIDTH *
(float32(currentLife) + hb.damageTaken) / float32(maxLife)
shake := rl.NewVector2(
randomShake(0.4)*hb.damageTaken,
randomShake(0.4)*hb.damageTaken,
)
position = rl.Vector2Add(position, shake)
rl.DrawRectangleV(position, damageTakenBar, rl.Orange)
rl.DrawRectangleV(position, size, rl.Red)
hb.lastUpdate = rl.GetTime()
hb.life = currentLife
hb.damageTaken *= 0.95
}
func newHealthBar(buildings []Building) *healthBar {
return &healthBar{life: buildingsHealth * len(buildings)}
}
func (b *building) Update() {}
func (b *building) SphereCollision(centerSphere rl.Vector3, radiusSphere float32) bool {
return rl.CheckCollisionBoxSphere(b.boundingBox, centerSphere, radiusSphere)
}
func (b *building) RayCollision(ray rl.Ray) rl.RayCollision {
return rl.GetRayCollisionBox(ray, b.boundingBox)
}
func (b *building) Life() int {
return b.life
}
func (b *building) Animation() animationFunc {
return b.animate
}
func (b *building) Animate() animationOver {
return b.animate()
}
func (b *building) SetAnimation(f animationFunc) {
b.animate = f
}
func (b *building) CauseDamage(damage int) {
if b.animate != nil || b.life <= 0 {
return
}
b.animate = shakeBuilding(b)
b.life -= damage
}
func (b *building) IsAlive() bool {
return b.life > 0 || b.animate != nil
}
func newBuilding(x, z float32) *building {
color := rl.NewColor(
uint8(rl.GetRandomValue(0, 180)),
uint8(rl.GetRandomValue(0, 180)),
uint8(rl.GetRandomValue(0, 180)),
255,
)
pos := rl.NewVector3(x*buildingSpacing, 0, z*buildingSpacing)
size := rl.NewVector3(
buildingSize,
float32(rl.GetRandomValue(1, maxBuildingHeight)),
buildingSize,
)
min := rl.NewVector3(
pos.X-buildingSize/2,
0,
pos.Z-buildingSize/2,
)
max := rl.NewVector3(
pos.X+buildingSize/2,
size.Y/2,
pos.Z+buildingSize/2,
)
return &building{
color: color,
life: buildingsHealth,
object: object{pos: pos},
size: size,
boundingBox: rl.NewBoundingBox(min, max),
}
}
func drawWireframe(building building) {
increasedSize := rl.Vector3Scale(building.size, 1.05)
invertedColor := building.color
invertedColor.R += 127
invertedColor.G += 127
invertedColor.B += 127
rl.DrawCubeWiresV(
building.pos,
increasedSize,
invertedColor,
)
}
func shakeEnemy(enemy *shootingEnemy) animationFunc {
duration := newTimer(0.7)
return func() animationOver {
if 0 == 0 {
if !enemy.IsAlive() {
enemy.color = rl.NewColor(255, 0, 0, 255)
} else {
enemy.color = rl.RayWhite
}
}
shake := rl.NewVector3(
rand.Float32()*.2-.1,
rand.Float32()*.1-.05,
rand.Float32()*.2-.1,
)
rl.DrawSphere(rl.Vector3Add(enemy.Pos(), shake), 1, rl.Red)
return animationOver(duration.ttr())
}
}
func shakeBuilding(b *building) animationFunc {
duration := newTimer(0.7)
t := newTimer(0.08)
return func() animationOver {
building := *b
if t.ttr() {
if b.life == 0 {
building.color = rl.NewColor(255, 0, 0, 255)
} else {
building.color = rl.RayWhite
}
}
drawWireframe(building)
shake := rl.NewVector3(
rand.Float32()*.2-.1,
rand.Float32()*.1-.05,
rand.Float32()*.2-.1,
)
shake = rl.Vector3Scale(shake, float32(math.Sin(rl.GetTime())+1)/3)
rl.DrawCubeV(
rl.Vector3Add(building.pos, shake),
building.size,
building.color,
)
return animationOver(duration.ttr())
}
}
func (b *building) Draw() {
hasAnimation := b.Animation() != nil
if b.Life() <= 0 && !hasAnimation {
return
}
if hasAnimation {
if over := b.Animate(); over {
b.SetAnimation(nil)
}
return
}
drawWireframe(*b)
rl.DrawCubeV(b.pos, b.size, b.color)
}
func (c *character) laserBeam() {
if !rl.IsMouseButtonDown(rl.MouseLeftButton) {
return
}
ray := rl.Ray{
Position: c.camera.Position,
Direction: rl.Vector3Subtract(c.camera.Target, c.camera.Position),
}
closestDistance := math.Inf(1)
var closest Building
for _, building := range city.Buildings() {
if !building.IsAlive() {
continue
}
playerDistance := rl.Vector3Distance(c.camera.Position, building.Pos())
if playerDistance < float32(closestDistance) {
collision := building.RayCollision(ray)
if collision.Hit {
closestDistance = float64(playerDistance)
closest = building
}
}
}
if closest != nil {
closest.CauseDamage(c.damage)
}
}
func newBuildings() []Building {
var buildings []Building
mb := int(math.Sqrt(totalBuildings))
for i := 0; i < mb; i++ {
for j := 0; j < mb; j++ {
buildings = append(buildings, newBuilding(float32(i)-10, float32(j)-10))
}
}
return buildings
}
func newBuildingBlocks() []Building {
var buildings []Building
for i := 0; i < blocksNumber; i++ {
for j := 0; j < blocksNumber; j++ {
for k := 0; k < buildsPerBlock; k++ {
for l := 0; l < buildsPerBlock; l++ {
buildings = append(buildings, newBuilding(float32(i), float32(j)))
}
}
}
}
return buildings
}
func drawHUD() {
city.healthBar.Draw()
rl.DrawText(
fmt.Sprintf("buildings destroyed: %d", 1024-len(city.Buildings())), MARGIN, MARGIN, 20, rl.White,
)
rl.DrawText(fmt.Sprintf("frame time: %f", rl.GetFrameTime()), MARGIN, MARGIN+MARGIN, 20, rl.White)
}
func spawnEnemies() []DrawableLivingThing {
var ret []DrawableLivingThing
for range 20 {
e := NewShootingEnemy(rl.NewVector3(rand.Float32()*400-200, 2, rand.Float32()*400-200), 6)
ret = append(ret, e)
}
return ret
}
func main() {
rl.SetConfigFlags(rl.FlagMsaa4xHint)
rl.InitWindow(WIDTH, HEIGHT, "raylib [core] example - 3d camera first person")
rl.SetTargetFPS(120)
rl.HideCursor()
rl.DisableCursor()
rl.SetMousePosition(WIDTH/2, HEIGHT/2)
character := newCharacter()
buildings := newBuildings()
healthBar := newHealthBar(buildings)
city = &scene{healthBar: healthBar, character: character}
// Linguagem burra
var b []DrawableLivingThing = make([]DrawableLivingThing, len(buildings))
for i, bu := range buildings {
b[i] = bu
}
city.AppendThing(spawnEnemies()...)
city.AppendThing(b...)
city.AppendThing(character)
for !rl.WindowShouldClose() {
city.Update()
city.Draw()
}
rl.CloseWindow()
}