aka gravity ninja golf

collisions ig

+233 -124
+174 -99
game.gd
··· 19 19 var FORCE_POWER: float = 1.5 20 20 var MY_FRICTION: float = .9999 21 21 var N_FIELDS = 64 22 - var N_POINTS = 100 22 + var N_POINTS = 8 23 23 24 24 var startp: Vector2 25 25 var endp: Vector2 ··· 33 33 34 34 var debug = false 35 35 var firing = false 36 - var playerv: Vector2 37 - var playerc: Color = orange 38 - var playerdrag = 1.0 39 - var trail = [] 36 + @onready var activeStone = $Stone 37 + @onready var stones: Array[Stone] = [$Stone] 40 38 41 39 42 40 ## BOUNDS is the screen bounds, dimensions of the game window or smth ··· 71 69 for i in range(N_FIELDS): 72 70 ff[i] = genfield() 73 71 74 - func rendervecfield(): 72 + func rendervecfield(): 73 + 75 74 debugstarts = [] 76 75 debugends = [] 77 76 # pregenerate debug field bc it's a bit costly to do each frame, and doesn't change unless ff changes ··· 84 83 debugends.append(Vector2(x,y)+ v) 85 84 86 85 func _ready(): 87 - $Player.initialize(BOUNDS) 88 - $Player.position = startp 89 - $Player.modulate = playerc 90 - $Mouse.modulate = playerc 86 + $Stone.initialize(BOUNDS) 87 + $Stone.position = startp 88 + $Stone.modulate = green 89 + $Mouse.modulate = green 91 90 $TitleScreen.initialize(green, yellow, orange, red, BOUNDS) 92 - $Target.initialize(white, black, BOUNDS) 91 + $Target.initialize(white, BOUNDS) 93 92 $Target.position = endp 94 93 95 94 ## genpoints generates 2 points that we intend to be at least target (toroidal) distance apart ··· 119 118 var showdebug = false 120 119 121 120 func _process(delta: float): 122 - if Input.is_action_just_pressed("click"): 123 - $TitleScreen.hide() 124 - if Input.is_action_just_pressed("debug"): 125 - debug = !debug 126 - hidedebug = Input.is_action_pressed("hide") 127 - showdebug = Input.is_action_pressed("show") 128 - if Input.is_action_just_pressed("regenerate"): 129 - genfields() 130 - rendervecfield() 131 - if Input.is_action_pressed("fields_up"): 132 - N_FIELDS += 1 133 - if Input.is_action_pressed("fields_down"): 134 - N_FIELDS -= 1 135 - if Input.is_action_just_pressed("avg_up"): 136 - last_avg = FORCE_AVG 137 - if Input.is_action_just_pressed("avg_down"): 138 - last_avg = FORCE_AVG 139 - if Input.is_action_pressed("avg_up"): 140 - FORCE_AVG *= 1.005 141 - if Input.is_action_pressed("avg_down"): 142 - FORCE_AVG *= .995 143 - if Input.is_action_just_released("avg_up") || Input.is_action_just_released("avg_down"): 144 - var del = FORCE_AVG - last_avg 145 - for i in range(ff.size()): 146 - var f = ff[i] 147 - f.z = f.z + del 148 - ff[i] = f 149 - rendervecfield() 150 - if Input.is_action_just_pressed("dev_up"): 151 - last_dev = FORCE_DEV 152 - if Input.is_action_just_pressed("dev_down"): 153 - last_dev = FORCE_DEV 154 - if Input.is_action_pressed("dev_up"): 155 - FORCE_DEV *= 1.005 156 - if Input.is_action_pressed("dev_down"): 157 - FORCE_DEV *= .995 158 - if Input.is_action_just_released("dev_up") || Input.is_action_just_released("dev_down"): 159 - var ratio = FORCE_DEV / last_dev 160 - for i in range(ff.size()): 161 - var f = ff[i] 162 - f.z = FORCE_AVG + (f.z-FORCE_AVG) * ratio 163 - ff[i] = f 164 - rendervecfield() 165 - if Input.is_action_pressed("friction_up"): 166 - MY_FRICTION *= .99999 167 - if Input.is_action_pressed("friction_down"): 168 - MY_FRICTION = lerp(MY_FRICTION, 1.0, delta) 169 - if Input.is_action_pressed("power_up"): 170 - FORCE_POWER *= 1.0005 171 - if Input.is_action_pressed("power_down"): 172 - FORCE_POWER *= .9995 173 - if Input.is_action_just_released("power_up"): 174 - rendervecfield() 175 - if Input.is_action_just_released("power_down"): 176 - rendervecfield() 121 + do_debug_inputs(delta) 177 122 178 123 t += delta 179 124 if t > .24: 180 125 $Mouse.sketch() 181 - $Player.sketch() 126 + for stone in stones: 127 + stone.sketch() 182 128 $Target.sketch() 183 129 t = 0 184 130 185 131 mp = get_viewport().get_mouse_position() 186 132 $Mouse.set_target(mp, delta) 187 133 if Input.is_action_pressed("click") && !firing: 188 - var correctHeading = tDist($Player.position, mp) 189 - $Player.set_heading(correctHeading) 134 + var correctHeading = tDist(activeStone.position, mp) 135 + activeStone.set_heading(correctHeading) 190 136 if Input.is_action_just_released("click") && !firing: 191 - var correctHeading = tDist($Player.position, mp) 192 - $Player.confirm_heading(correctHeading) 137 + var correctHeading = tDist(activeStone.position, mp) 138 + activeStone.confirm_heading(correctHeading) 193 139 194 - if Input.is_action_just_pressed("fire") && !firing && $Player.hconfirmed: 195 - var h = $Player.fire() 140 + if Input.is_action_just_pressed("fire") && !firing && activeStone.hconfirmed: 141 + var h = activeStone.fire() 196 142 firing = true 197 - trail = [$Player.position] 198 - playerv = h 199 - playerdrag = 1.0 143 + activeStone.velocity = h 144 + activeStone.drag = 1.0 145 + 146 + 200 147 148 + for stone in stones: 149 + if stone.mystate == Stone.state.INIT: 150 + continue 151 + stone.velocity = stone.velocity + forces(stone.position) * delta 152 + stone.velocity = stone.velocity * stone.drag 201 153 if firing: 202 - playerv += forces($Player.position) * delta 203 - $Player.position = tNorm($Player.position + delta * playerv) 204 - trail.append($Player.position) 205 - playerv *= playerdrag 206 - playerdrag *= MY_FRICTION 207 - if playerv.length() < 10.0: 154 + activeStone.drag = activeStone.drag * MY_FRICTION 155 + updatePositions(delta) 156 + if firing: 157 + if activeStone.velocity.length() < 10.0: 158 + activeStone.drag = .99 159 + var ns = Stone.new() 160 + ns.position = genpoint(SAFEBOUNDS) 161 + ns.modulate = [green, yellow, orange, red].pick_random() 162 + add_child(ns) 163 + activeStone = ns 164 + stones.append(ns) 208 165 firing = false 209 166 MY_FRICTION = .9999 167 + 210 168 211 169 # physics simulation 212 170 for i in range(N_POINTS): ··· 223 181 # position to get increased accuracy 224 182 vv[i] += delta*forces(pp[i]) 225 183 226 - # ds = v*dt, blah blah same first order stuff 184 + ## ds = v*dt, blah blah same first order stuff 227 185 pp[i] = tNorm(pp[i] + delta*vv[i]) 186 + 228 187 229 188 # this is some friction air resistance stuff, it makes everything much more interesting to look at 230 189 vv[i] *= .99 190 + #updatePositions(pp, vv, delta) 231 191 queue_redraw() 232 192 233 193 func _draw(): 234 - 235 - 236 194 if debug: 237 195 for i in range(min(debugstarts.size(), debugends.size())): 238 196 draw_line(debugstarts[i], debugends[i], gencolor(debugstarts[i], debugends[i]), 1, false) 239 - 240 - 241 - for i in range(trail.size()-1): 242 - var u = trail[i] 243 - var v = trail[i + 1] 244 - if !tNormConnected(u,v): continue 245 - draw_line(u, v, playerc, 1) 246 197 247 198 for i in range(N_POINTS): 248 199 var c = white 249 200 c.a = abs(pcpc[i]) 250 - draw_line(pp[i], pp[i] + vv[i].normalized()*2, c, 2) 201 + draw_circle(pp[i], 11.0, c, 2) 251 202 252 203 if (debug && !hidedebug) || showdebug: 253 204 var dstring = "debug: d; hide: h" ··· 352 303 dy = dy - BOUNDS.size.y 353 304 elif abs(dy) > abs(dy + BOUNDS.size.y): 354 305 dy = dy + BOUNDS.size.y 355 - #var dx = abs(to.x - from.x) 356 - #var dy = abs(to.y - from.y) 357 - #if dx > 0.5 * BOUNDS.size.x: 358 - #dx = BOUNDS.size.x - dx 359 - #if dy > 0.5 * BOUNDS.size.y: 360 - #dy = BOUNDS.size.y - dy 361 306 return Vector2(dx,dy) 362 307 308 + func updatePositions(delta: float): 309 + var remaining: float = delta 310 + while remaining > 0: 311 + var pcollisiont: Array[float] = [] 312 + var pcollisions: Array[Vector2i] = [] 313 + for i in range(stones.size()): 314 + for j in range(i + 1,stones.size()): 315 + # https://stackoverflow.com/questions/18410937/resolving-a-circle-circle-collision 316 + var p = tDist(stones[i].position, stones[j].position) 317 + if p.length() < 22.0: 318 + var res = tCollision(stones[i].position, stones[j].position, stones[i].velocity, stones[j].velocity) 319 + stones[i].velocity = -Vector2(res.x, res.y) 320 + stones[j].velocity = -Vector2(res.z, res.w) 321 + continue 322 + var v = stones[i].velocity - stones[j].velocity 323 + var a = v.dot(v) 324 + var b = 2 * p.dot(v) 325 + var c = p.dot(p) - 22.0*22.0 326 + var discriminant = b*b - 4 * a * c 327 + if discriminant < 0: 328 + continue 329 + var time1 = (-b - sqrt(discriminant)) / (2 * a) 330 + var time2 = (-b + sqrt(discriminant)) / (2 * a) 331 + var time = time1 332 + if time1 < 0: 333 + time = time2 334 + if time < 0: 335 + continue 336 + if time > remaining: 337 + continue 338 + else: 339 + pcollisiont.append(time) 340 + pcollisions.append(Vector2i(i,j)) 341 + var least = remaining 342 + var leasti = -1 343 + var leastj = -1 344 + for i in range(pcollisions.size()): 345 + if pcollisiont[i] < least: 346 + least = pcollisiont[i] 347 + leasti = pcollisions[i].x 348 + leastj = pcollisions[i].y 349 + remaining -= least 350 + for i in range(stones.size()): 351 + stones[i].position = tNorm(stones[i].position + stones[i].velocity*least) 352 + if leasti > -1: 353 + print("clink", leasti, leastj) 354 + var res = tCollision(stones[leasti].position, stones[leastj].position, stones[leasti].velocity, stones[leastj].velocity) 355 + stones[leasti].velocity = -Vector2(res.x, res.y) 356 + stones[leastj].velocity = -Vector2(res.z, res.w) 357 + print("done") 358 + 359 + #for i in range(stones.size()): 360 + #for j in range(i + 1, stones.size()): 361 + #var d = tDist(stones[i].position, stones[j].position) 362 + #if d.length() < 21.0: 363 + #var bump = (22.5 - d.length()) / 2 364 + #stones[i].position = stones[i].position - bump * d.normalized() 365 + #stones[j].position = stones[j].position + bump * d.normalized() 366 + 367 + 368 + func tCollision(apos: Vector2, bpos: Vector2, avel: Vector2, bvel: Vector2): 369 + var d = tDist(apos, bpos) 370 + var n = d.normalized() 371 + var vrel = avel - bvel 372 + var vn = vrel.dot(n) 373 + if vn > 0: 374 + return Vector4(avel.x, avel.y, bvel.x, bvel.y) 375 + var avelf = avel - vn * n 376 + var bvelf = bvel + vn * n 377 + return Vector4(avelf.x, avelf.y, bvelf.x, bvelf.y) 378 + 363 379 ## tNorm is a bad name which takes [param p] and puts it within bounds, used whenever we move points and might cause 364 380 ## them to wrap around. might not be necessary for things which we don't render, but i think tDist requires 365 381 ## that both points are in the main universe so you probably should use me ··· 378 394 379 395 func tNormConnected(u: Vector2, v: Vector2) -> bool: 380 396 return abs(tDist(u, v).length_squared() - (v - u).length_squared()) < 10 397 + 398 + 399 + func do_debug_inputs(delta: float): 400 + if Input.is_action_just_pressed("click"): 401 + $TitleScreen.hide() 402 + if Input.is_action_just_pressed("debug"): 403 + debug = !debug 404 + hidedebug = Input.is_action_pressed("hide") 405 + showdebug = Input.is_action_pressed("show") 406 + if Input.is_action_just_pressed("regenerate"): 407 + genfields() 408 + rendervecfield() 409 + if Input.is_action_pressed("fields_up"): 410 + N_FIELDS += 1 411 + if Input.is_action_pressed("fields_down"): 412 + N_FIELDS -= 1 413 + if Input.is_action_just_pressed("avg_up"): 414 + last_avg = FORCE_AVG 415 + if Input.is_action_just_pressed("avg_down"): 416 + last_avg = FORCE_AVG 417 + if Input.is_action_pressed("avg_up"): 418 + FORCE_AVG *= 1.005 419 + if Input.is_action_pressed("avg_down"): 420 + FORCE_AVG *= .995 421 + if Input.is_action_just_released("avg_up") || Input.is_action_just_released("avg_down"): 422 + var del = FORCE_AVG - last_avg 423 + for i in range(ff.size()): 424 + var f = ff[i] 425 + f.z = f.z + del 426 + ff[i] = f 427 + rendervecfield() 428 + if Input.is_action_just_pressed("dev_up"): 429 + last_dev = FORCE_DEV 430 + if Input.is_action_just_pressed("dev_down"): 431 + last_dev = FORCE_DEV 432 + if Input.is_action_pressed("dev_up"): 433 + FORCE_DEV *= 1.005 434 + if Input.is_action_pressed("dev_down"): 435 + FORCE_DEV *= .995 436 + if Input.is_action_just_released("dev_up") || Input.is_action_just_released("dev_down"): 437 + var ratio = FORCE_DEV / last_dev 438 + for i in range(ff.size()): 439 + var f = ff[i] 440 + f.z = FORCE_AVG + (f.z-FORCE_AVG) * ratio 441 + ff[i] = f 442 + rendervecfield() 443 + if Input.is_action_pressed("friction_up"): 444 + MY_FRICTION *= .99999 445 + if Input.is_action_pressed("friction_down"): 446 + MY_FRICTION = lerp(MY_FRICTION, 1.0, delta) 447 + if Input.is_action_pressed("power_up"): 448 + FORCE_POWER *= 1.0005 449 + if Input.is_action_pressed("power_down"): 450 + FORCE_POWER *= .9995 451 + if Input.is_action_just_released("power_up"): 452 + rendervecfield() 453 + if Input.is_action_just_released("power_down"): 454 + rendervecfield() 455 +
+7 -3
game.tscn
··· 1 - [gd_scene load_steps=6 format=3 uid="uid://b2knoqyv0re16"] 1 + [gd_scene load_steps=7 format=3 uid="uid://b2knoqyv0re16"] 2 2 3 3 [ext_resource type="Script" uid="uid://dynw8lf5xoin1" path="res://game.gd" id="1_80nbo"] 4 4 [ext_resource type="Script" uid="uid://bylorvm6k6ce7" path="res://mouse.gd" id="2_e2o6t"] 5 5 [ext_resource type="Script" uid="uid://dfmhnkdxb2scm" path="res://title_screen.gd" id="2_fc0e3"] 6 6 [ext_resource type="Script" uid="uid://c17pumsu3f5o" path="res://target.gd" id="3_7jktm"] 7 - [ext_resource type="Script" uid="uid://cvixjye57xwtt" path="res://player.gd" id="3_feb5d"] 7 + [ext_resource type="Script" uid="uid://cvixjye57xwtt" path="res://stone.gd" id="3_feb5d"] 8 + [ext_resource type="Script" uid="uid://dwtq2ucbceoy6" path="res://heading.gd" id="6_ryrav"] 8 9 9 10 [node name="Node2D" type="Node2D"] 10 11 script = ExtResource("1_80nbo") ··· 22 23 [node name="Mouse" type="Node2D" parent="."] 23 24 script = ExtResource("2_e2o6t") 24 25 25 - [node name="Player" type="Node2D" parent="."] 26 + [node name="Stone" type="Node2D" parent="."] 26 27 script = ExtResource("3_feb5d") 28 + 29 + [node name="Heading" type="Node2D" parent="."] 30 + script = ExtResource("6_ryrav")
+1
heading.gd
··· 1 + extends Node2D
+1
heading.gd.uid
··· 1 + uid://dwtq2ucbceoy6
+17 -1
player.gd stone.gd
··· 1 + class_name Stone 1 2 extends Node2D 2 3 3 4 ## vheading is our currently visible heading ··· 7 8 ## heading is the actual heading we will use for gameplay 8 9 var heading: Vector2 9 10 ## maxlen is the maxlen that we can fire towards, perhaps this changes over the course of a game 10 - var maxlen: float = 200 11 + const maxlen: float = 200 11 12 var BOUNDS: Rect2 13 + 14 + var velocity: Vector2 15 + var drag: float 16 + var trail: Array[Vector2] 17 + 18 + enum state { 19 + INIT, 20 + FIRED, 21 + DONE 22 + } 23 + var mystate: state = state.INIT 12 24 13 25 var mypoly: PackedVector2Array 14 26 var mycolors: PackedColorArray ··· 57 69 hconfirmed = false 58 70 heading = Vector2.ZERO 59 71 vheading = Vector2.ZERO 72 + mystate = state.FIRED 73 + trail = [] 74 + drag = 1.0 60 75 queue_redraw() 61 76 return h 77 + 62 78 63 79 ## set_heading is called while we have our mouse down, and it truncates our length towards maxlen, with some 64 80 ## polynomial falloff
player.gd.uid stone.gd.uid
+33 -21
target.gd
··· 2 2 3 3 var polyinner: PackedVector2Array 4 4 var colyinner: PackedColorArray 5 - var polyminner: PackedVector2Array 6 - var colyminner: PackedColorArray 7 - var polymouter: PackedVector2Array 8 - var colymouter: PackedColorArray 9 5 var polyouter: PackedVector2Array 10 6 var colyouter: PackedColorArray 11 7 var BOUNDS: Rect2 12 8 13 - func initialize(fg: Color, bg: Color, bounds: Rect2): 9 + func initialize(fg: Color, bounds: Rect2): 14 10 colyinner = [] 15 - colyinner.resize(11) 16 - colyinner.fill(bg) 17 - colyminner = [] 18 - colyminner.resize(14) 19 - colyminner.fill(fg) 20 - colymouter = [] 21 - colymouter.resize(19) 22 - colymouter.fill(bg) 11 + colyinner.resize(11 + 14 + 2) 12 + colyinner.fill(fg) 13 + 23 14 colyouter = [] 24 - colyouter.resize(23) 15 + colyouter.resize(23 + 19 + 2) 25 16 colyouter.fill(fg) 26 17 BOUNDS = bounds 27 18 sketch() 28 19 29 20 func sketch(): 30 - polyinner = genpolygon(11, 14.0, 1.0) 31 - polyminner = genpolygon(14, 28.0, 1.1) 32 - polymouter = genpolygon(19, 50.0, 1.3) 33 - polyouter = genpolygon(23, 62.0, 1.5) 21 + polyinner = genring(genpolygon(11, 14.0, 1.0), genpolygon(14, 28.0, 1.1)) 22 + polyouter = genring(genpolygon(19, 50.0, 1.3), genpolygon(23, 62.0, 1.5)) 34 23 queue_redraw() 35 24 36 25 func _draw(): 37 26 for dx in [-1, 0, 1]: 38 27 for dy in [-1, 0, 1]: 39 28 draw_polygon(offsetpoly(polyouter, dx, dy), colyouter) 40 - draw_polygon(offsetpoly(polymouter, dx, dy), colymouter) 41 - draw_polygon(offsetpoly(polyminner, dx, dy), colyminner) 42 29 draw_polygon(offsetpoly(polyinner, dx, dy), colyinner) 43 30 44 31 func offsetpoly(a: PackedVector2Array, dx: int, dy: int) -> PackedVector2Array: ··· 52 39 res[i] = a[i] + offset 53 40 return res 54 41 42 + ## genring takes an [param inner] polygon and an [param outer] polygon, and concatenates them so that 43 + ## they form a polygon with the inner polygon cut out from the outer polygon. doesn't do any checks 44 + ## to make sure that the polygon actually tesselates, but it does pick the best point in the inner polygon 45 + ## to connect to the first point from the outer polygon 46 + func genring(inner: PackedVector2Array, outer: PackedVector2Array) -> PackedVector2Array: 47 + var res: PackedVector2Array = [] 48 + res.resize(inner.size() + outer.size() + 2) 49 + for i in range(outer.size()): 50 + res[i] = outer[i] 51 + var fouter = outer[0] 52 + res[outer.size()] = fouter 53 + var bi: int = -1 54 + var best: float = 100000.0 55 + for j in range(inner.size()): 56 + var nl = (fouter - inner[j]).length() 57 + if nl < best: 58 + best = nl 59 + bi = j 60 + var os = outer.size() + 1 61 + for j in range(inner.size()): 62 + var idx = (inner.size() - j + bi) % inner.size() 63 + res[os + j] = inner[idx] 64 + res[os + inner.size()] = inner[bi] 65 + return res 66 + 55 67 ## genpolygon generates a (nonconvex polygon) with [param n] vertices, and the given [param radius] 56 68 ## and [param deviation] 57 69 func genpolygon(n: int, radius: float, deviation: float) -> PackedVector2Array: ··· 62 74 var rsum = 0 63 75 for i in range(n): 64 76 rads[i] = randfn(radius, deviation) 65 - rots[i] = rsum + randf() 77 + rots[i] = rsum + randf() + .2 66 78 rsum = rots[i] 67 79 var poly = PackedVector2Array() 68 80 poly.resize(n)