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 kotlin.random.Random
21 
22 // artificially speed up or slow down the simulation
23 const val TIME_SCALE = 1f // simulation seconds per wall clock second
24 
25 // if it's been over 1 real second since our last timestep, don't simulate that elapsed time.
26 // this allows the simulation to "pause" when, for example, the activity pauses
27 const val MAX_VALID_DT = 1f
28 
29 interface Entity {
30     // Integrate.
31     // Compute accelerations from forces, add accelerations to velocity, save old position,
32     // add velocity to position.
updatenull33     fun update(sim: Simulator, dt: Float)
34 
35     // Post-integration step, after constraints are satisfied.
36     fun postUpdate(sim: Simulator, dt: Float)
37 }
38 
39 interface Removable {
40     fun canBeRemoved(): Boolean
41 }
42 
43 class Fuse(var lifetime: Float) : Removable {
updatenull44     fun update(dt: Float) {
45         lifetime -= dt
46     }
canBeRemovednull47     override fun canBeRemoved(): Boolean {
48         return lifetime < 0
49     }
50 }
51 
52 open class Body(var name: String = "Unknown") : Entity {
53     var pos = Vec2.Zero
54     var opos = Vec2.Zero
55     var velocity = Vec2.Zero
56 
57     var mass = 0f
58     var angle = 0f
59     var radius = 0f
60 
61     var collides = true
62 
63     var omega: Float
64         get() = angle - oangle
65         set(value) {
66             oangle = angle - value
67         }
68 
69     var oangle = 0f
70 
updatenull71     override fun update(sim: Simulator, dt: Float) {
72         if (dt <= 0) return
73 
74         // integrate velocity
75         val vscaled = velocity * dt
76         opos = pos
77         pos += vscaled
78 
79         // integrate angular velocity
80         //        val wscaled = omega * timescale
81         //        oangle = angle
82         //        angle = (angle + wscaled) % PI2f
83     }
84 
postUpdatenull85     override fun postUpdate(sim: Simulator, dt: Float) {
86         if (dt <= 0) return
87         velocity = (pos - opos) / dt
88     }
89 }
90 
91 interface Constraint {
92     // Solve constraints. Pick up objects and put them where they are "supposed" to be.
solvenull93     fun solve(sim: Simulator, dt: Float)
94 }
95 
96 open class Container(val radius: Float) : Constraint {
97     private val list = ArraySet<Body>()
98     private val softness = 0.0f
99 
100     override fun toString(): String {
101         return "Container($radius)"
102     }
103 
104     fun add(p: Body) {
105         list.add(p)
106     }
107 
108     fun remove(p: Body) {
109         list.remove(p)
110     }
111 
112     override fun solve(sim: Simulator, dt: Float) {
113         for (p in list) {
114             if ((p.pos.mag() + p.radius) > radius) {
115                 p.pos =
116                     p.pos * (softness) +
117                         Vec2.makeWithAngleMag(p.pos.angle(), radius - p.radius) * (1f - softness)
118             }
119         }
120     }
121 }
122 
123 open class Simulator(val randomSeed: Long) {
124     private var wallClockNanos: Long = 0L
125     var now: Float = 0f
126     var dt: Float = 0f
127     val rng = Random(randomSeed)
128     val entities = ArraySet<Entity>(1000)
129     val constraints = ArraySet<Constraint>(100)
130 
addnull131     fun add(e: Entity) = entities.add(e)
132     fun remove(e: Entity) = entities.remove(e)
133     fun add(c: Constraint) = constraints.add(c)
134     fun remove(c: Constraint) = constraints.remove(c)
135 
136     open fun updateAll(dt: Float, entities: ArraySet<Entity>) {
137         entities.forEach { it.update(this, dt) }
138     }
139 
solveAllnull140     open fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
141         constraints.forEach { it.solve(this, dt) }
142     }
143 
postUpdateAllnull144     open fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
145         entities.forEach { it.postUpdate(this, dt) }
146     }
147 
stepnull148     fun step(nanos: Long) {
149         val firstFrame = (wallClockNanos == 0L)
150 
151         dt = (nanos - wallClockNanos) / 1_000_000_000f * TIME_SCALE
152         this.wallClockNanos = nanos
153 
154         // we start the simulation on the next frame
155         if (firstFrame || dt > MAX_VALID_DT) return
156 
157         // simulation is running; we start accumulating simulation time
158         this.now += dt
159 
160         val localEntities = ArraySet(entities)
161         val localConstraints = ArraySet(constraints)
162 
163         // position-based dynamics approach:
164         // 1. apply acceleration to velocity, save positions, apply velocity to position
165         updateAll(dt, localEntities)
166 
167         // 2. solve all constraints
168         solveAll(dt, localConstraints)
169 
170         // 3. compute new velocities from updated positions and saved positions
171         postUpdateAll(dt, localEntities)
172     }
173 }
174