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