1 /*
<lambda>null2 * 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 LANDING_REMOVAL_TIME = 60 * 15f // 15 min of simulation time
47
48 const val SCALED_THRUST = true
49
50 open class Planet(
51 val orbitCenter: Vec2,
52 radius: Float,
53 pos: Vec2,
54 var speed: Float,
55 var color: Color = Color.White
56 ) : Body() {
57 var atmosphere = ""
58 var description = ""
59 var flora = ""
60 var fauna = ""
61 var explored = false
62 private val orbitRadius: Float
63 init {
64 this.radius = radius
65 this.pos = pos
66 orbitRadius = pos.distance(orbitCenter)
67 mass = 4 / 3 * PIf * radius.pow(3) * PLANETARY_DENSITY
68 }
69
70 override fun update(sim: Simulator, dt: Float) {
71 val orbitAngle = (pos - orbitCenter).angle()
72 // constant linear velocity
73 velocity = Vec2.makeWithAngleMag(orbitAngle + PIf / 2f, speed)
74
75 super.update(sim, dt)
76 }
77
78 override fun postUpdate(sim: Simulator, dt: Float) {
79 // This is kind of like a constraint, but whatever.
80 val orbitAngle = (pos - orbitCenter).angle()
81 pos = orbitCenter + Vec2.makeWithAngleMag(orbitAngle, orbitRadius)
82 super.postUpdate(sim, dt)
83 }
84 }
85
86 enum class StarClass {
87 O,
88 B,
89 A,
90 F,
91 G,
92 K,
93 M
94 }
95
starColornull96 fun starColor(cls: StarClass) =
97 when (cls) {
98 StarClass.O -> Color(0xFF6666FF)
99 StarClass.B -> Color(0xFFCCCCFF)
100 StarClass.A -> Color(0xFFEEEEFF)
101 StarClass.F -> Color(0xFFFFFFFF)
102 StarClass.G -> Color(0xFFFFFF66)
103 StarClass.K -> Color(0xFFFFCC33)
104 StarClass.M -> Color(0xFFFF8800)
105 }
106
107 class Star(val cls: StarClass, radius: Float) :
108 Planet(orbitCenter = Vec2.Zero, radius = radius, pos = Vec2.Zero, speed = 0f) {
109 init {
110 pos = Vec2.Zero
111 mass = 4 / 3 * PIf * radius.pow(3) * STELLAR_DENSITY
112 color = starColor(cls)
113 collides = false
114 }
115 var anim = 0f
updatenull116 override fun update(sim: Simulator, dt: Float) {
117 anim += dt
118 }
119 }
120
121 open class Universe(val namer: Namer, randomSeed: Long) : Simulator(randomSeed) {
122 var latestDiscovery: Planet? = null
123 lateinit var star: Star
124 lateinit var ship: Spacecraft
125 val planets: MutableList<Planet> = mutableListOf()
126 var follow: Body? = null
127 val ringfence = Container(UNIVERSE_RANGE)
128
initTestnull129 fun initTest() {
130 val systemName = "TEST SYSTEM"
131 star =
132 Star(
133 cls = StarClass.A,
134 radius = STAR_RADIUS_RANGE.endInclusive,
135 )
136 .apply { name = "TEST SYSTEM" }
137
138 repeat(NUM_PLANETS_RANGE.last) {
139 val thisPlanetFrac = it.toFloat() / (NUM_PLANETS_RANGE.last - 1)
140 val radius =
141 lerp(PLANET_RADIUS_RANGE.start, PLANET_RADIUS_RANGE.endInclusive, thisPlanetFrac)
142 val orbitRadius =
143 lerp(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive, thisPlanetFrac)
144
145 val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT
146 val speed = 2f * PIf * orbitRadius / period
147
148 val p =
149 Planet(
150 orbitCenter = star.pos,
151 radius = radius,
152 pos = star.pos + Vec2.makeWithAngleMag(thisPlanetFrac * PI2f, orbitRadius),
153 speed = speed,
154 color = Colors.Eigengrau4
155 )
156 android.util.Log.v("Landroid", "created planet $p with period $period and vel $speed")
157 val num = it + 1
158 p.description = "TEST PLANET #$num"
159 p.atmosphere = "radius=$radius"
160 p.flora = "mass=${p.mass}"
161 p.fauna = "speed=$speed"
162 planets.add(p)
163 add(p)
164 }
165
166 planets.sortBy { it.pos.distance(star.pos) }
167 planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" }
168 add(star)
169
170 ship = Spacecraft()
171
172 // in the test universe, start the ship near the outermost planet
173 ship.pos = planets.last().pos + Vec2(planets.first().radius * 1.5f, 0f)
174
175 ship.angle = 0f
176 add(ship)
177
178 ringfence.add(ship)
179 add(ringfence)
180
181 follow = ship
182 }
183
initRandomnull184 fun initRandom() {
185 val systemName = namer.nameSystem(rng)
186 star =
187 Star(
188 cls = rng.choose(StarClass.values()),
189 radius = rng.nextFloatInRange(STAR_RADIUS_RANGE)
190 )
191 star.name = systemName
192 repeat(rng.nextInt(NUM_PLANETS_RANGE.first, NUM_PLANETS_RANGE.last + 1)) {
193 val radius = rng.nextFloatInRange(PLANET_RADIUS_RANGE)
194 val orbitRadius =
195 lerp(
196 PLANET_ORBIT_RANGE.start,
197 PLANET_ORBIT_RANGE.endInclusive,
198 rng.nextFloat().pow(1f)
199 )
200
201 // Kepler's third law
202 val period = sqrt(orbitRadius.pow(3f) / star.mass) * KEPLER_CONSTANT
203 val speed = 2f * PIf * orbitRadius / period
204
205 val p =
206 Planet(
207 orbitCenter = star.pos,
208 radius = radius,
209 pos = star.pos + Vec2.makeWithAngleMag(rng.nextFloat() * PI2f, orbitRadius),
210 speed = speed,
211 color = Colors.Eigengrau4
212 )
213 android.util.Log.v("Landroid", "created planet $p with period $period and vel $speed")
214 p.description = namer.describePlanet(rng)
215 p.atmosphere = namer.describeAtmo(rng)
216 p.flora = namer.describeLife(rng)
217 p.fauna = namer.describeLife(rng)
218 planets.add(p)
219 add(p)
220 }
221 planets.sortBy { it.pos.distance(star.pos) }
222 planets.forEachIndexed { idx, planet -> planet.name = "$systemName ${idx + 1}" }
223 add(star)
224
225 ship = Spacecraft()
226
227 ship.pos =
228 star.pos +
229 Vec2.makeWithAngleMag(
230 rng.nextFloat() * PI2f,
231 rng.nextFloatInRange(PLANET_ORBIT_RANGE.start, PLANET_ORBIT_RANGE.endInclusive)
232 )
233 ship.angle = rng.nextFloat() * PI2f
234 add(ship)
235
236 ringfence.add(ship)
237 add(ringfence)
238
239 follow = ship
240 }
241
updateAllnull242 override fun updateAll(dt: Float, entities: ArraySet<Entity>) {
243 // check for passing in front of the sun
244 ship.transit = false
245
246 (planets + star).forEach { planet ->
247 val vector = planet.pos - ship.pos
248 val d = vector.mag()
249 if (d < planet.radius) {
250 if (planet is Star) ship.transit = true
251 } else if (
252 now > ship.launchClock + LAUNCH_MECO
253 ) { // within MECO sec of launch, no gravity at all
254 // simulate gravity: $ f_g = G * m1 * m2 * 1/d^2 $
255 ship.velocity =
256 ship.velocity +
257 Vec2.makeWithAngleMag(
258 vector.angle(),
259 GRAVITATION * (ship.mass * planet.mass) / d.pow(2)
260 ) * dt
261 }
262 }
263
264 super.updateAll(dt, entities)
265 }
266
closestPlanetnull267 fun closestPlanet(): Planet {
268 val bodiesByDist =
269 (planets + star)
270 .map { planet -> (planet.pos - ship.pos) to planet }
271 .sortedBy { it.first.mag() }
272
273 return bodiesByDist[0].second
274 }
275
solveAllnull276 override fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
277 if (ship.landing == null) {
278 val planet = closestPlanet()
279
280 if (planet.collides) {
281 val d = (ship.pos - planet.pos).mag() - ship.radius - planet.radius
282 val a = (ship.pos - planet.pos).angle()
283
284 if (d < 0) {
285 // landing, or impact?
286
287 // 1. relative speed
288 val vDiff = (ship.velocity - planet.velocity).mag()
289 // 2. landing angle
290 val aDiff = (ship.angle - a).absoluteValue
291
292 // landing criteria
293 if (aDiff < PIf / 4
294 // &&
295 // vDiff < 100f
296 ) {
297 val landing = Landing(ship, planet, a, namer.describeActivity(rng, planet))
298 ship.landing = landing
299 ship.velocity = planet.velocity
300 add(landing)
301
302 planet.explored = true
303 latestDiscovery = planet
304 } else {
305 val impact = planet.pos + Vec2.makeWithAngleMag(a, planet.radius)
306 ship.pos =
307 planet.pos + Vec2.makeWithAngleMag(a, planet.radius + ship.radius - d)
308
309 // add(Spark(
310 // lifetime = 1f,
311 // style = Spark.Style.DOT,
312 // color = Color.Yellow,
313 // size = 10f
314 // ).apply {
315 // pos = impact
316 // opos = impact
317 // velocity = Vec2.Zero
318 // })
319 //
320 (1..10).forEach {
321 Spark(
322 ttl = rng.nextFloatInRange(0.5f, 2f),
323 style = Spark.Style.DOT,
324 color = Color.White,
325 size = 1f
326 )
327 .apply {
328 pos =
329 impact +
330 Vec2.makeWithAngleMag(
331 rng.nextFloatInRange(0f, 2 * PIf),
332 rng.nextFloatInRange(0.1f, 0.5f)
333 )
334 opos = pos
335 velocity =
336 ship.velocity * 0.8f +
337 Vec2.makeWithAngleMag(
338 // a +
339 // rng.nextFloatInRange(-PIf, PIf),
340 rng.nextFloatInRange(0f, 2 * PIf),
341 rng.nextFloatInRange(0.1f, 0.5f)
342 )
343 add(this)
344 }
345 }
346 }
347 }
348 }
349 }
350
351 super.solveAll(dt, constraints)
352 }
353
postUpdateAllnull354 override fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
355 super.postUpdateAll(dt, entities)
356
357 entities
358 .filterIsInstance<Removable>()
359 .filter(predicate = Removable::canBeRemoved)
360 .forEach { remove(it as Entity) }
361
362 constraints
363 .filterIsInstance<Removable>()
364 .filter(predicate = Removable::canBeRemoved)
365 .forEach { remove(it as Constraint) }
366 }
367 }
368
369 class Landing(
370 var ship: Spacecraft?,
371 val planet: Planet,
372 val angle: Float,
373 val text: String = "",
374 private val fuse: Fuse = Fuse(LANDING_REMOVAL_TIME)
<lambda>null375 ) : Constraint, Removable by fuse {
376 override fun solve(sim: Simulator, dt: Float) {
377 ship?.let { ship ->
378 val landingVector = Vec2.makeWithAngleMag(angle, ship.radius + planet.radius)
379 val desiredPos = planet.pos + landingVector
380 ship.pos = (ship.pos * 0.5f) + (desiredPos * 0.5f) // @@@ FIXME
381 ship.angle = angle
382 }
383
384 fuse.update(dt)
385 }
386 }
387
388 class Spark(
389 var ttl: Float,
390 collides: Boolean = false,
391 mass: Float = 0f,
392 val style: Style = Style.LINE,
393 val color: Color = Color.Gray,
394 val size: Float = 2f,
395 val fuse: Fuse = Fuse(ttl)
396 ) : Removable by fuse, Body(name = "Spark") {
397 enum class Style {
398 LINE,
399 LINE_ABSOLUTE,
400 DOT,
401 DOT_ABSOLUTE,
402 RING
403 }
404
405 init {
406 this.collides = collides
407 this.mass = mass
408 }
updatenull409 override fun update(sim: Simulator, dt: Float) {
410 super.update(sim, dt)
411 fuse.update(dt)
412 }
413 }
414
415 const val TRACK_LENGTH = 10_000
416 const val SIMPLE_TRACK_DRAWING = true
417
418 class Track {
419 val positions = ArrayDeque<Vec2>(TRACK_LENGTH)
420 private val angles = ArrayDeque<Float>(TRACK_LENGTH)
addnull421 fun add(x: Float, y: Float, a: Float) {
422 if (positions.size >= (TRACK_LENGTH - 1)) {
423 positions.removeFirst()
424 angles.removeFirst()
425 positions.removeFirst()
426 angles.removeFirst()
427 }
428 positions.addLast(Vec2(x, y))
429 angles.addLast(a)
430 }
431 }
432
433 class Spacecraft : Body() {
434 var thrust = Vec2.Zero
435 var launchClock = 0f
436
437 var transit = false
438
439 val track = Track()
440
441 var landing: Landing? = null
442 var autopilot: Autopilot? = null
443
444 init {
445 mass = SPACECRAFT_MASS
446 radius = 12f
447 }
448
updatenull449 override fun update(sim: Simulator, dt: Float) {
450 // check for thrusters
451 val thrustMag = thrust.mag()
452 if (thrustMag > 0) {
453 var deltaV = MAIN_ENGINE_ACCEL * dt
454 if (SCALED_THRUST) deltaV *= thrustMag.coerceIn(0f, 1f)
455
456 // check if we are currently attached to a landing
457 landing?.let { landing ->
458 // launch clock is 1 second long
459 if (launchClock == 0f) launchClock = sim.now + 1f /* @@@ TODO extract */
460
461 if (sim.now > launchClock) {
462 // detach from landing site
463 landing.ship = null
464 this.landing = null
465 } else {
466 deltaV = 0f
467 }
468 }
469
470 // this is it. impart thrust to the ship.
471 // note that we always thrust in the forward direction
472 velocity += Vec2.makeWithAngleMag(angle, deltaV)
473 } else {
474 if (launchClock != 0f) launchClock = 0f
475 }
476
477 // apply global speed limit
478 if (velocity.mag() > CRAFT_SPEED_LIMIT)
479 velocity = Vec2.makeWithAngleMag(velocity.angle(), CRAFT_SPEED_LIMIT)
480
481 super.update(sim, dt)
482 }
483
postUpdatenull484 override fun postUpdate(sim: Simulator, dt: Float) {
485 super.postUpdate(sim, dt)
486
487 // special effects all need to be added after the simulation step so they have
488 // the correct position of the ship.
489 track.add(pos.x, pos.y, angle)
490
491 val mag = thrust.mag()
492 if (sim.rng.nextFloat() < mag) {
493 // exhaust
494 sim.add(
495 Spark(
496 ttl = sim.rng.nextFloatInRange(0.5f, 1f),
497 collides = true,
498 mass = 1f,
499 style = Spark.Style.RING,
500 size = 1f,
501 color = Color(0x40FFFFFF)
502 )
503 .also { spark ->
504 spark.pos = pos
505 spark.opos = pos
506 spark.velocity =
507 velocity +
508 Vec2.makeWithAngleMag(
509 angle + sim.rng.nextFloatInRange(-0.2f, 0.2f),
510 -MAIN_ENGINE_ACCEL * mag * 10f * dt
511 )
512 }
513 )
514 }
515 }
516 }
517