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