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() }