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