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
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.
33     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 open class Body(var name: String = "Unknown") : Entity {
40     var pos = Vec2.Zero
41     var opos = Vec2.Zero
42     var velocity = Vec2.Zero
43 
44     var mass = 0f
45     var angle = 0f
46     var radius = 0f
47 
48     var collides = true
49 
50     var omega: Float
51         get() = angle - oangle
52         set(value) {
53             oangle = angle - value
54         }
55 
56     var oangle = 0f
57 
58     override fun update(sim: Simulator, dt: Float) {
59         if (dt <= 0) return
60 
61         // integrate velocity
62         val vscaled = velocity * dt
63         opos = pos
64         pos += vscaled
65 
66         // integrate angular velocity
67         //        val wscaled = omega * timescale
68         //        oangle = angle
69         //        angle = (angle + wscaled) % PI2f
70     }
71 
72     override fun postUpdate(sim: Simulator, dt: Float) {
73         if (dt <= 0) return
74         velocity = (pos - opos) / dt
75     }
76 }
77 
78 interface Constraint {
79     // Solve constraints. Pick up objects and put them where they are "supposed" to be.
80     fun solve(sim: Simulator, dt: Float)
81 }
82 
83 open class Container(val radius: Float) : Constraint {
84     private val list = ArraySet<Body>()
85     private val softness = 0.0f
86 
87     override fun toString(): String {
88         return "Container($radius)"
89     }
90 
91     fun add(p: Body) {
92         list.add(p)
93     }
94 
95     fun remove(p: Body) {
96         list.remove(p)
97     }
98 
99     override fun solve(sim: Simulator, dt: Float) {
100         for (p in list) {
101             if ((p.pos.mag() + p.radius) > radius) {
102                 p.pos =
103                     p.pos * (softness) +
104                         Vec2.makeWithAngleMag(p.pos.angle(), radius - p.radius) * (1f - softness)
105             }
106         }
107     }
108 }
109 
110 open class Simulator(val randomSeed: Long) {
111     private var wallClockNanos: Long = 0L
112     var now: Float = 0f
113     var dt: Float = 0f
114     val rng = Random(randomSeed)
115     val entities = ArraySet<Entity>(1000)
116     val constraints = ArraySet<Constraint>(100)
117 
118     fun add(e: Entity) = entities.add(e)
119     fun remove(e: Entity) = entities.remove(e)
120     fun add(c: Constraint) = constraints.add(c)
121     fun remove(c: Constraint) = constraints.remove(c)
122 
123     open fun updateAll(dt: Float, entities: ArraySet<Entity>) {
124         entities.forEach { it.update(this, dt) }
125     }
126 
127     open fun solveAll(dt: Float, constraints: ArraySet<Constraint>) {
128         constraints.forEach { it.solve(this, dt) }
129     }
130 
131     open fun postUpdateAll(dt: Float, entities: ArraySet<Entity>) {
132         entities.forEach { it.postUpdate(this, dt) }
133     }
134 
135     fun step(nanos: Long) {
136         val firstFrame = (wallClockNanos == 0L)
137 
138         dt = (nanos - wallClockNanos) / 1_000_000_000f * TIME_SCALE
139         this.wallClockNanos = nanos
140 
141         // we start the simulation on the next frame
142         if (firstFrame || dt > MAX_VALID_DT) return
143 
144         // simulation is running; we start accumulating simulation time
145         this.now += dt
146 
147         val localEntities = ArraySet(entities)
148         val localConstraints = ArraySet(constraints)
149 
150         // position-based dynamics approach:
151         // 1. apply acceleration to velocity, save positions, apply velocity to position
152         updateAll(dt, localEntities)
153 
154         // 2. solve all constraints
155         solveAll(dt, localConstraints)
156 
157         // 3. compute new velocities from updated positions and saved positions
158         postUpdateAll(dt, localEntities)
159     }
160 }
161