• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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