1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.egg.landroid 18 19 import android.util.ArraySet 20 import androidx.compose.ui.graphics.Color 21 import androidx.compose.ui.util.lerp 22 import kotlin.math.absoluteValue 23 import kotlin.math.pow 24 import kotlin.math.sqrt 25 26 const val UNIVERSE_RANGE = 200_000f 27 28 val NUM_PLANETS_RANGE = 1..10 29 val STAR_RADIUS_RANGE = (1_000f..8_000f) 30 val PLANET_RADIUS_RANGE = (50f..2_000f) 31 val PLANET_ORBIT_RANGE = (STAR_RADIUS_RANGE.endInclusive * 2f)..(UNIVERSE_RANGE * 0.75f) 32 33 const val GRAVITATION = 1e-2f 34 const val KEPLER_CONSTANT = 50f // * 4f * PIf * PIf / GRAVITATION 35 36 // m = d * r 37 const val PLANETARY_DENSITY = 2.5f 38 const val STELLAR_DENSITY = 0.5f 39 40 const val SPACECRAFT_MASS = 10f 41 42 const val CRAFT_SPEED_LIMIT = 5_000f 43 const val MAIN_ENGINE_ACCEL = 1000f // thrust effect, pixels per second squared 44 const val LAUNCH_MECO = 2f // how long to suspend gravity when launching 45 46 const val SCALED_THRUST = true 47 48 interface Removable { 49 fun canBeRemoved(): Boolean 50 } 51 52 open class Planet( 53 val orbitCenter: Vec2, 54 radius: Float, 55 pos: Vec2, 56 var speed: Float, 57 var color: Color = Color.White 58 ) : Body() { 59 var atmosphere = "" 60 var description = "" 61 var flora = "" 62 var fauna = "" 63 var explored = false 64 private val orbitRadius: Float 65 init { 66 this.radius = radius 67 this.pos = pos 68 orbitRadius = pos.distance(orbitCenter) 69 mass = 4 / 3 * PIf * radius.pow(3) * PLANETARY_DENSITY 70 } 71 72 override fun update(sim: Simulator, dt: Float) { 73 val orbitAngle = (pos - orbitCenter).angle() 74 // constant linear velocity 75 velocity = Vec2.makeWithAngleMag(orbitAngle + PIf / 2f, speed) 76 77 super.update(sim, dt) 78 } 79 80 override fun postUpdate(sim: Simulator, dt: Float) { 81 // This is kind of like a constraint, but whatever. 82 val orbitAngle = (pos - orbitCenter).angle() 83 pos = orbitCenter + Vec2.makeWithAngleMag(orbitAngle, orbitRadius) 84 super.postUpdate(sim, dt) 85 } 86 } 87 88 enum class StarClass { 89 O, 90 B, 91 A, 92 F, 93 G, 94 K, 95 M 96 } 97 98 fun starColor(cls: StarClass) = 99 when (cls) { 100 StarClass.O -> Color(0xFF6666FF) 101 StarClass.B -> Color(0xFFCCCCFF) 102 StarClass.A -> Color(0xFFEEEEFF) 103 StarClass.F -> Color(0xFFFFFFFF) 104 StarClass.G -> Color(0xFFFFFF66) 105 StarClass.K -> Color(0xFFFFCC33) 106 StarClass.M -> Color(0xFFFF8800) 107 } 108 109 class Star(val cls: StarClass, radius: Float) : 110 Planet(orbitCenter = Vec2.Zero, radius = radius, pos = Vec2.Zero, speed = 0f) { 111 init { 112 pos = Vec2.Zero 113 mass = 4 / 3 * PIf * radius.pow(3) * STELLAR_DENSITY 114 color = starColor(cls) 115 collides = false 116 } 117 var anim = 0f 118 override fun update(sim: Simulator, dt: Float) { 119 anim += dt 120 } 121 } 122 123 open class Universe(val namer: Namer, randomSeed: Long) : Simulator(randomSeed) { 124 var latestDiscovery: Planet? = null 125 lateinit var star: Star 126 lateinit var ship: Spacecraft 127 val planets: MutableList<Planet> = mutableListOf() 128 var follow: Body? = null 129 val ringfence = Container(UNIVERSE_RANGE) 130 131 fun initTest() { 132 val systemName = "TEST SYSTEM" 133 star = 134 Star( 135 cls = StarClass.A, 136 radius = STAR_RADIUS_RANGE.endInclusive, 137 ) 138 .apply { name = "TEST SYSTEM" } 139 140 repeat(NUM_PLANETS_RANGE.last) { 141 val thisPlanetFrac = it.toFloat() / (NUM_PLANETS_RANGE.last - 1) 142 val radius = 143 lerp(PLANET_RADIUS_RANGE.start, PLANET_RADIUS_RANGE.endInclusive, thisPlanetFrac) 144 val orbitRadius = 145 lerp(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive, thisPlanetFrac) 146 147 val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT 148 val speed = 2f * PIf * orbitRadius / period 149 150 val p = 151 Planet( 152 orbitCenter = star.pos, 153 radius = radius, 154 pos = star.pos + Vec2.makeWithAngleMag(thisPlanetFrac * PI2f, orbitRadius), 155 speed = speed, 156 color = Colors.Eigengrau4 157 ) 158 android.util.Log.v( 159 "Landroid", 160 "created planet $p with period $period and vel $speed" 161 ) 162 val num = it + 1 163 p.description = "TEST PLANET #$num" 164 p.atmosphere = "radius=$radius" 165 p.flora = "mass=${p.mass}" 166 p.fauna = "speed=$speed" 167 planets.add(p) 168 add(p) 169 } 170 171 planets.sortBy { it.pos.distance(star.pos) } 172 planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" } 173 add(star) 174 175 ship = Spacecraft() 176 177 ship.pos = star.pos + Vec2.makeWithAngleMag(PIf / 4, PLANET_ORBIT_RANGE.start) 178 ship.angle = 0f 179 add(ship) 180 181 ringfence.add(ship) 182 add(ringfence) 183 184 follow = ship 185 } 186 187 fun initRandom() { 188 val systemName = namer.nameSystem(rng) 189 star = 190 Star( 191 cls = rng.choose(StarClass.values()), 192 radius = rng.nextFloatInRange(STAR_RADIUS_RANGE) 193 ) 194 star.name = systemName 195 repeat(rng.nextInt(NUM_PLANETS_RANGE.first, NUM_PLANETS_RANGE.last + 1)) { 196 val radius = rng.nextFloatInRange(PLANET_RADIUS_RANGE) 197 val orbitRadius = 198 lerp( 199 PLANET_ORBIT_RANGE.start, 200 PLANET_ORBIT_RANGE.endInclusive, 201 rng.nextFloat().pow(1f) 202 ) 203 204 // Kepler's third law 205 val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT 206 val speed = 2f * PIf * orbitRadius / period 207 208 val p = 209 Planet( 210 orbitCenter = star.pos, 211 radius = radius, 212 pos = star.pos + Vec2.makeWithAngleMag(rng.nextFloat() * PI2f, orbitRadius), 213 speed = speed, 214 color = Colors.Eigengrau4 215 ) 216 android.util.Log.v( 217 "Landroid", 218 "created planet $p with period $period and vel $speed" 219 ) 220 p.description = namer.describePlanet(rng) 221 p.atmosphere = namer.describeAtmo(rng) 222 p.flora = namer.describeLife(rng) 223 p.fauna = namer.describeLife(rng) 224 planets.add(p) 225 add(p) 226 } 227 planets.sortBy { it.pos.distance(star.pos) } 228 planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" } 229 add(star) 230 231 ship = Spacecraft() 232 233 ship.pos = 234 star.pos + 235 Vec2.makeWithAngleMag( 236 rng.nextFloat() * PI2f, 237 rng.nextFloatInRange(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive) 238 ) 239 ship.angle = rng.nextFloat() * PI2f 240 add(ship) 241 242 ringfence.add(ship) 243 add(ringfence) 244 245 follow = ship 246 } 247 248 override fun updateAll(dt: Float, entities: ArraySet<Entity>) { 249 // check for passing in front of the sun 250 ship.transit = false 251 252 (planets + star).forEach { planet -> 253 val vector = planet.pos - ship.pos 254 val d = vector.mag() 255 if (d < planet.radius) { 256 if (planet is Star) ship.transit = true 257 } else if ( 258 now > ship.launchClock + LAUNCH_MECO 259 ) { // within MECO sec of launch, no gravity at all 260 // simulate gravity: $ f_g = G * m1 * m2 * 1/d^2 $ 261 ship.velocity = 262 ship.velocity + 263 Vec2.makeWithAngleMag( 264 vector.angle(), 265 GRAVITATION * (ship.mass * planet.mass) / d.pow(2) 266 ) * dt 267 } 268 } 269 270 super.updateAll(dt, entities) 271 } 272 273 fun closestPlanet(): Planet { 274 val bodiesByDist = 275 (planets + star) 276 .map { planet -> (planet.pos - ship.pos) to planet } 277 .sortedBy { it.first.mag() } 278 279 return bodiesByDist[0].second 280 } 281 282 override fun solveAll(dt: Float, constraints: ArraySet<Constraint>) { 283 if (ship.landing == null) { 284 val planet = closestPlanet() 285 286 if (planet.collides) { 287 val d = (ship.pos - planet.pos).mag() - ship.radius - planet.radius 288 val a = (ship.pos - planet.pos).angle() 289 290 if (d < 0) { 291 // landing, or impact? 292 293 // 1. relative speed 294 val vDiff = (ship.velocity - planet.velocity).mag() 295 // 2. landing angle 296 val aDiff = (ship.angle - a).absoluteValue 297 298 // landing criteria 299 if (aDiff < PIf / 4 300 // && 301 // vDiff < 100f 302 ) { 303 val landing = Landing(ship, planet, a) 304 ship.landing = landing 305 ship.velocity = planet.velocity 306 add(landing) 307 308 planet.explored = true 309 latestDiscovery = planet 310 } else { 311 val impact = planet.pos + Vec2.makeWithAngleMag(a, planet.radius) 312 ship.pos = 313 planet.pos + Vec2.makeWithAngleMag(a, planet.radius + ship.radius - d) 314 315 // add(Spark( 316 // lifetime = 1f, 317 // style = Spark.Style.DOT, 318 // color = Color.Yellow, 319 // size = 10f 320 // ).apply { 321 // pos = impact 322 // opos = impact 323 // velocity = Vec2.Zero 324 // }) 325 // 326 (1..10).forEach { 327 Spark( 328 lifetime = rng.nextFloatInRange(0.5f, 2f), 329 style = Spark.Style.DOT, 330 color = Color.White, 331 size = 1f 332 ) 333 .apply { 334 pos = 335 impact + 336 Vec2.makeWithAngleMag( 337 rng.nextFloatInRange(0f, 2 * PIf), 338 rng.nextFloatInRange(0.1f, 0.5f) 339 ) 340 opos = pos 341 velocity = 342 ship.velocity * 0.8f + 343 Vec2.makeWithAngleMag( 344 // a + 345 // rng.nextFloatInRange(-PIf, PIf), 346 rng.nextFloatInRange(0f, 2 * PIf), 347 rng.nextFloatInRange(0.1f, 0.5f) 348 ) 349 add(this) 350 } 351 } 352 } 353 } 354 } 355 } 356 357 super.solveAll(dt, constraints) 358 } 359 360 override fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) { 361 super.postUpdateAll(dt, entities) 362 363 entities 364 .filterIsInstance<Removable>() 365 .filter(predicate = Removable::canBeRemoved) 366 .filterIsInstance<Entity>() 367 .forEach { remove(it) } 368 } 369 } 370 371 class Landing(val ship: Spacecraft, val planet: Planet, val angle: Float) : Constraint { 372 private val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius) 373 override fun solve(sim: Simulator, dt: Float) { 374 val desiredPos = planet.pos + landingVector 375 ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME 376 ship.angle = angle 377 } 378 } 379 380 class Spark( 381 var lifetime: Float, 382 collides: Boolean = false, 383 mass: Float = 0f, 384 val style: Style = Style.LINE, 385 val color: Color = Color.Gray, 386 val size: Float = 2f 387 ) : Removable, Body() { 388 enum class Style { 389 LINE, 390 LINE_ABSOLUTE, 391 DOT, 392 DOT_ABSOLUTE, 393 RING 394 } 395 396 init { 397 this.collides = collides 398 this.mass = mass 399 } 400 override fun update(sim: Simulator, dt: Float) { 401 super.update(sim, dt) 402 lifetime -= dt 403 } 404 override fun canBeRemoved(): Boolean { 405 return lifetime < 0 406 } 407 } 408 409 const val TRACK_LENGTH = 10_000 410 const val SIMPLE_TRACK_DRAWING = true 411 412 class Track { 413 val positions = ArrayDeque<Vec2>(TRACK_LENGTH) 414 private val angles = ArrayDeque<Float>(TRACK_LENGTH) 415 fun add(x: Float, y: Float, a: Float) { 416 if (positions.size >= (TRACK_LENGTH - 1)) { 417 positions.removeFirst() 418 angles.removeFirst() 419 positions.removeFirst() 420 angles.removeFirst() 421 } 422 positions.addLast(Vec2(x, y)) 423 angles.addLast(a) 424 } 425 } 426 427 class Spacecraft : Body() { 428 var thrust = Vec2.Zero 429 var launchClock = 0f 430 431 var transit = false 432 433 val track = Track() 434 435 var landing: Landing? = null 436 437 init { 438 mass = SPACECRAFT_MASS 439 radius = 12f 440 } 441 442 override fun update(sim: Simulator, dt: Float) { 443 // check for thrusters 444 val thrustMag = thrust.mag() 445 if (thrustMag > 0) { 446 var deltaV = MAIN_ENGINE_ACCEL * dt 447 if (SCALED_THRUST) deltaV *= thrustMag.coerceIn(0f, 1f) 448 449 if (landing == null) { 450 // we are free in space, so we attempt to pivot toward the desired direction 451 // NOTE: no longer required thanks to FlightStick 452 // angle = thrust.angle() 453 } else 454 landing?.let { landing -> 455 if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */ 456 457 if (sim.now > launchClock) { 458 // first-stage to orbit has 1000x power 459 // deltaV *= 1000f 460 sim.remove(landing) 461 this.landing = null 462 } else { 463 deltaV = 0f 464 } 465 } 466 467 // this is it. impart thrust to the ship. 468 // note that we always thrust in the forward direction 469 velocity += Vec2.makeWithAngleMag(angle, deltaV) 470 } else { 471 if (launchClock != 0f) launchClock = 0f 472 } 473 474 // apply global speed limit 475 if (velocity.mag() > CRAFT_SPEED_LIMIT) 476 velocity = Vec2.makeWithAngleMag(velocity.angle(), CRAFT_SPEED_LIMIT) 477 478 super.update(sim, dt) 479 } 480 481 override fun postUpdate(sim: Simulator, dt: Float) { 482 super.postUpdate(sim, dt) 483 484 // special effects all need to be added after the simulation step so they have 485 // the correct position of the ship. 486 track.add(pos.x, pos.y, angle) 487 488 val mag = thrust.mag() 489 if (sim.rng.nextFloat() < mag) { 490 // exhaust 491 sim.add( 492 Spark( 493 lifetime = sim.rng.nextFloatInRange(0.5f, 1f), 494 collides = true, 495 mass = 1f, 496 style = Spark.Style.RING, 497 size = 3f, 498 color = Color(0x40FFFFFF) 499 ) 500 .also { spark -> 501 spark.pos = pos 502 spark.opos = pos 503 spark.velocity = 504 velocity + 505 Vec2.makeWithAngleMag( 506 angle + sim.rng.nextFloatInRange(-0.2f, 0.2f), 507 -MAIN_ENGINE_ACCEL * mag * 10f * dt 508 ) 509 } 510 ) 511 } 512 } 513 } 514