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 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
updatenull72 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
postUpdatenull80 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
starColornull98 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
updatenull118 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
initTestnull131 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
initRandomnull187 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
updateAllnull248 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
closestPlanetnull273 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
solveAllnull282 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
postUpdateAllnull360 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)
solvenull373 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 }
updatenull400 override fun update(sim: Simulator, dt: Float) {
401 super.update(sim, dt)
402 lifetime -= dt
403 }
canBeRemovednull404 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)
addnull415 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
updatenull442 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
postUpdatenull481 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