• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.util.fastForEach
21 import kotlinx.coroutines.DisposableHandle
22 import kotlin.random.Random
23 
24 // artificially speed up or slow down the simulation
25 const val TIME_SCALE = 1f // simulation seconds per wall clock second
26 
27 // if it's been over 1 real second since our last timestep, don't simulate that elapsed time.
28 // this allows the simulation to "pause" when, for example, the activity pauses
29 const val MAX_VALID_DT = 1f
30 
31 interface Entity {
32     // Integrate.
33     // Compute accelerations from forces, add accelerations to velocity, save old position,
34     // add velocity to position.
updatenull35     fun update(sim: Simulator, dt: Float)
36 
37     // Post-integration step, after constraints are satisfied.
38     fun postUpdate(sim: Simulator, dt: Float)
39 }
40 
41 interface Removable {
42     fun canBeRemoved(): Boolean
43 }
44 
45 class Fuse(var lifetime: Float) : Removable {
updatenull46     fun update(dt: Float) {
47         lifetime -= dt
48     }
canBeRemovednull49     override fun canBeRemoved(): Boolean {
50         return lifetime < 0
51     }
52 }
53 
54 open class Body(var name: String = "Unknown") : Entity {
55     var pos = Vec2.Zero
56     var opos = Vec2.Zero
57     var velocity = Vec2.Zero
58 
59     var mass = 0f
60     var angle = 0f
61     var radius = 0f
62 
63     var collides = true
64 
65     var omega: Float
66         get() = angle - oangle
67         set(value) {
68             oangle = angle - value
69         }
70 
71     var oangle = 0f
72 
updatenull73     override fun update(sim: Simulator, dt: Float) {
74         if (dt <= 0) return
75 
76         // integrate velocity
77         val vscaled = velocity * dt
78         opos = pos
79         pos += vscaled
80 
81         // integrate angular velocity
82         //        val wscaled = omega * timescale
83         //        oangle = angle
84         //        angle = (angle + wscaled) % PI2f
85     }
86 
postUpdatenull87     override fun postUpdate(sim: Simulator, dt: Float) {
88         if (dt <= 0) return
89         velocity = (pos - opos) / dt
90     }
91 }
92 
93 interface Constraint {
94     // Solve constraints. Pick up objects and put them where they are "supposed" to be.
solvenull95     fun solve(sim: Simulator, dt: Float)
96 }
97 
98 open class Container(val radius: Float) : Constraint {
99     private val list = ArraySet<Body>()
100     private val softness = 0.0f
101 
102     override fun toString(): String {
103         return "Container($radius)"
104     }
105 
106     fun add(p: Body) {
107         list.add(p)
108     }
109 
110     fun remove(p: Body) {
111         list.remove(p)
112     }
113 
114     override fun solve(sim: Simulator, dt: Float) {
115         for (p in list) {
116             if ((p.pos.mag() + p.radius) > radius) {
117                 p.pos =
118                     p.pos * (softness) +
119                         Vec2.makeWithAngleMag(p.pos.angle(), radius - p.radius) * (1f - softness)
120             }
121         }
122     }
123 }
124 
125 open class Simulator(val randomSeed: Long) {
126     private var wallClockNanos: Long = 0L
127     var now: Float = 0f
128     var dt: Float = 0f
129     val rng = Random(randomSeed)
130     val entities = ArraySet<Entity>(1000)
131     val constraints = ArraySet<Constraint>(100)
132     private val simStepListeners = mutableListOf<() -> Unit>()
133 
addnull134     fun add(e: Entity) = entities.add(e)
135     fun remove(e: Entity) = entities.remove(e)
136     fun add(c: Constraint) = constraints.add(c)
137     fun remove(c: Constraint) = constraints.remove(c)
138 
139     open fun updateAll(dt: Float, entities: ArraySet<Entity>) {
140         entities.forEach { it.update(this, dt) }
141     }
142 
solveAllnull143     open fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
144         constraints.forEach { it.solve(this, dt) }
145     }
146 
postUpdateAllnull147     open fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
148         entities.forEach { it.postUpdate(this, dt) }
149     }
150 
stepnull151     fun step(nanos: Long) {
152         val firstFrame = (wallClockNanos == 0L)
153 
154         dt = (nanos - wallClockNanos) / 1_000_000_000f * TIME_SCALE
155         this.wallClockNanos = nanos
156 
157         // we start the simulation on the next frame
158         if (firstFrame || dt > MAX_VALID_DT) return
159 
160         // simulation is running; we start accumulating simulation time
161         this.now += dt
162 
163         val localEntities = ArraySet(entities)
164         val localConstraints = ArraySet(constraints)
165 
166         // position-based dynamics approach:
167         // 1. apply acceleration to velocity, save positions, apply velocity to position
168         updateAll(dt, localEntities)
169 
170         // 2. solve all constraints
171         solveAll(dt, localConstraints)
172 
173         // 3. compute new velocities from updated positions and saved positions
174         postUpdateAll(dt, localEntities)
175 
176         // 4. notify listeners that step is complete
177         simStepListeners.fastForEach { it.invoke() }
178     }
179 
180     /**
181      * Register [listener] to be invoked every time the [Simulator] completes one [step].
182      * Use this to enqueue drawing.
183      *
184      * Instead of the usual register()/unregister() pattern, we're going to borrow
185      * [kotlinx.coroutines.DisposableHandle] here. Call [DisposableHandle.dispose] on the return
186      * value to unregister.
187      */
addSimulationStepListenernull188     fun addSimulationStepListener(listener: () -> Unit): DisposableHandle {
189         // add to listener list
190         simStepListeners += listener
191 
192         return DisposableHandle {
193             // on dispose, remove from listener list
194             simStepListeners -= listener
195         }
196     }
197 }
198