aka gravity ninja golf
at main 458 lines 16 kB view raw
1extends Node2D 2 3## ff is the array of force field components (idk if theres a word for this in physics tbh) 4var ff: Array[Vector4] = [] 5## pp is the array of point locations 6var pp: Array[Vector2] = [] 7## pcpc is the array of point colors (well really just the alpha) 8var pcpc: Array[float] = [] 9## vv is the array of point velocities 10var vv: Array[Vector2] = [] 11## start is the start location (here) (probably gonna remove this) 12var mp 13## t is a time variable which we use to calculate when to resketch sketched objects 14var t = 0 15 16var FORCE_AVG: float = 15000.0 17var FORCE_DEV: float = 3000.0 18var FORCE_POWER: float = 1.5 19var MY_FRICTION: float = .9999 20var N_FIELDS = 64 21var N_POINTS = 100 22 23var startp: Vector2 24var endp: Vector2 25 26var debug = false 27var firing = false 28@onready var activeStone = null 29@onready var stones: Array[Stone] = [] 30 31## BOUNDS is the screen bounds, dimensions of the game window or smth 32var BOUNDS: Rect2 = ref.BOUNDS 33## SAFEBOUNDS is for some things which we don't want to generate right next to the side of the window 34var SAFEBOUNDS: Rect2 = ref.SAFEBOUNDS 35 36var debugstarts: Array[Vector2] = [] 37var debugends: Array[Vector2] = [] 38 39 40func generatelevel(): 41 genstartend() 42 genfields() 43 44@rpc("authority", "call_remote", "reliable") 45func server_transfer_level(_start: Vector2, _end: Vector2, _ff: PackedVector4Array): 46 pass 47 48 49func _init(): 50 genstartend() 51 genfields() 52 rendervecfield() 53 pp.resize(N_POINTS) 54 pcpc.resize(N_POINTS) 55 for i in range(N_POINTS): 56 pp[i] = genpoint() 57 pcpc[i] = 2*randf() - 1 58 59 vv.resize(N_POINTS) 60 vv.fill(Vector2.ZERO) 61 mp = Vector2.ZERO 62 63func genstartend(): 64 var pts = genpoints(550) 65 startp = Vector2(pts.x, pts.y) 66 endp = Vector2(pts.z, pts.w) 67 68func genfields(): 69 ff.resize(N_FIELDS) 70 for i in range(N_FIELDS): 71 ff[i] = genfield() 72 73func rendervecfield(): 74 75 debugstarts = [] 76 debugends = [] 77 # pregenerate debug field bc it's a bit costly to do each frame, and doesn't change unless ff changes 78 for x in range(BOUNDS.position.x, BOUNDS.position.x + BOUNDS.size.x, 8): 79 for y in range(BOUNDS.position.y, BOUNDS.position.y + BOUNDS.size.y, 8): 80 debugstarts.append(Vector2(x,y)) 81 var v = forces(Vector2(x,y))/10 82 if v.length() > 12: 83 v = v.normalized() * 12 84 debugends.append(Vector2(x,y)+ v) 85 86func _ready(): 87 $Target.position = endp 88 #$TitleScreen/Connect.clicked.connect(on_title_screen_clicked) 89 #$TitleScreen/Host.clicked.connect(on_title_screen_clicked) 90 91 92## genpoints generates 2 points that we intend to be at least target (toroidal) distance apart 93## within game bounds. 94## if it fails to generate 2 points at least target apart in maxtries, then it returns the two 95## farthest points from each other which it found. 96## returns points as a Vector4 (res.xy = p1, res.zw = p2) 97func genpoints(target: float, within: Rect2 = BOUNDS, maxtries: int = 20) -> Vector4: 98 var b1 = Vector2.ZERO 99 var b2 = Vector2.ZERO 100 var bd = 0 101 for i in range(maxtries): 102 var p1 = genpoint(within) 103 var p2 = genpoint(within) 104 var d = tDist(p1,p2).length() 105 if d > bd: 106 bd = d 107 b1 = p1 108 b2 = p2 109 if d > target: 110 break 111 return Vector4(b1.x, b1.y, b2.x, b2.y) 112 113var last_avg 114var last_dev 115var hidedebug = false 116var showdebug = false 117 118func on_title_screen_clicked(): 119 $TitleScreen.hide() 120 121func _process(delta: float): 122 do_debug_inputs(delta) 123 124 t += delta 125 if t > .24: 126 for stone in stones: 127 stone.sketch() 128 $Target.sketch() 129 t = 0 130 131 mp = get_viewport().get_mouse_position() 132 if Input.is_action_pressed("click") && !firing && activeStone: 133 var correctHeading = tDist(activeStone.position, mp) 134 activeStone.set_heading(correctHeading) 135 if Input.is_action_just_released("click") && !firing && activeStone: 136 var correctHeading = tDist(activeStone.position, mp) 137 activeStone.confirm_heading(correctHeading) 138 139 if Input.is_action_just_pressed("fire") && !firing && activeStone && activeStone.hconfirmed: 140 var h = activeStone.fire() 141 firing = true 142 activeStone.velocity = h 143 activeStone.drag = 1.0 144 145 # like if firing and sweep keys are just pressed 146 # we then will decrease drag and stuff and maybe rotate it a bit 147 148 149 for stone in stones: 150 if stone.mystate == Stone.state.INIT: 151 continue 152 #var sf = .5 153 #if stone == activeStone: 154 #sf = 1.0 155 #stone.velocity = stone.velocity + forces(stone.position) * delta * sf 156 stone.velocity = stone.velocity * stone.drag 157 if firing: 158 activeStone.drag = activeStone.drag * MY_FRICTION 159 updatePositions(delta) 160 if firing: 161 if activeStone.velocity.length() < 10.0: 162 activeStone.drag = .99 163 var ns = Stone.new() 164 ns.position = genpoint(SAFEBOUNDS) 165 ns.modulate = clr.num.pick_random() 166 add_child(ns) 167 activeStone = ns 168 stones.append(ns) 169 firing = false 170 MY_FRICTION = .9999 171 172 173 # physics simulation 174 for i in range(N_POINTS): 175 var a = pcpc[i] 176 if a > 0 && a - delta/10 < 0: 177 pp[i] = genpoint() 178 vv[i] = Vector2.ZERO 179 pcpc[i] -= .001 180 if pcpc[i] < -1: 181 pcpc[i] += 2 182 183 # f = ma, m is constant, so f=a, and then we can basically integrate first order approximation 184 # to get dv = f*dt, and just add the dv to existing vv. we can use runge-kutta for player 185 # position to get increased accuracy 186 vv[i] += delta*forces(pp[i]) 187 188 ## ds = v*dt, blah blah same first order stuff 189 pp[i] = tNorm(pp[i] + delta*vv[i]) 190 191 192 # this is some friction air resistance stuff, it makes everything much more interesting to look at 193 vv[i] *= .99 194 #updatePositions(pp, vv, delta) 195 queue_redraw() 196 197func _draw(): 198 if debug: 199 for i in range(min(debugstarts.size(), debugends.size())): 200 draw_line(debugstarts[i], debugends[i], gencolor(debugstarts[i], debugends[i]), 1, false) 201 202 for i in range(N_POINTS): 203 var c = clr.white 204 c.a = abs(pcpc[i]) 205 draw_circle(pp[i], 1.0, c, 2) 206 207 if (debug && !hidedebug) || showdebug: 208 var dstring = "debug: d; hide: h" 209 var ds = font.display.get_string_size(dstring) 210 draw_rect(Rect2(Vector2(16,16), ds), clr.black) 211 draw_string(font.display, Vector2(16,32), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 212 dstring = "regenerate: r" 213 ds = font.display.get_string_size(dstring) 214 draw_rect(Rect2(Vector2(16,36), ds), clr.black) 215 draw_string(font.display, Vector2(16,52), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 216 dstring = "N_FIELDS: (up: +, down: _) = %d" % N_FIELDS 217 ds = font.display.get_string_size(dstring) 218 draw_rect(Rect2(Vector2(16,56), ds), clr.black) 219 draw_string(font.display, Vector2(16,72), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 220 dstring = "FORCE_AVG: (up: }, down: {) = %f" % FORCE_AVG 221 ds = font.display.get_string_size(dstring) 222 draw_rect(Rect2(Vector2(16,76), ds), clr.black) 223 draw_string(font.display, Vector2(16,92), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 224 dstring = "FORCE_DEV: (up: >, down: <) = %f" % FORCE_DEV 225 ds = font.display.get_string_size(dstring) 226 draw_rect(Rect2(Vector2(16,96), ds), clr.black) 227 draw_string(font.display, Vector2(16,112), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 228 dstring = "MY_FRICTION: (up: UP_ARROW, down: DOWN_ARROW) = %f" % MY_FRICTION 229 ds = font.display.get_string_size(dstring) 230 draw_rect(Rect2(Vector2(16,116), ds), clr.black) 231 draw_string(font.display, Vector2(16,132), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 232 dstring = "FORCE_POWER: (up: RIGHT_ARROW, down: LEFT_ARROW) = %f" % FORCE_POWER 233 ds = font.display.get_string_size(dstring) 234 draw_rect(Rect2(Vector2(16,136), ds), clr.black) 235 draw_string(font.display, Vector2(16,152), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white) 236 237 238func gencolor(p: Vector2, v: Vector2) -> Color: 239 var s = v - p 240 var h = s.angle()/TAU 241 var c: Color 242 if h < -0.25: c = clr.green 243 elif h < 0: c = clr.yellow 244 elif h < 0.25: c = clr.orange 245 else: c = clr.red 246 c.a = 0.25 247 return c 248 249## genpoint generates a point. by default the point is wihin BOUNDS, but we can pick any Rect2 250func genpoint(within: Rect2 = BOUNDS) -> Vector2: 251 return within.position + Vector2(randf()*within.size.x, randf()*within.size.y) 252 253## genfield generates a force field component. res.xy is the origin of the force field component 254## r is the magnitude of the field 255## t is the angle that we are acting on 256func genfield() -> Vector4: 257 var o = genpoint() 258 var r = randfn(FORCE_AVG,FORCE_DEV) 259 var theta = randf() * TAU 260 return Vector4(o.x, o.y, r, theta) 261 262## force calcultaes the force on a point [param x] given the force field component [param f] [br] 263## we basically use an inverse relation, since the top is proportional in magnitude 264## to the distance between [param x] and the origin of [param f]. [br] 265## the way to think about r and theta are that r is the magnitude of the field, and r is the direction. [br] 266## there's a bit of redundant information since the angle can go all 2*pi radians, and the magnitude 267## can be negative, but we don't care. [br] 268## the way we calculate force is that we take the distance between [param x] and o as a vector, and then rotate 269## it according theta, and then scale it according to r. so say theta = pi/2 = 90deg, and r = 1 270## since the force is perpendicular to the displacement from the origin, we'd basically be rotated 271## around the point. if theta is 120deg, and r was 1 then we'd spiral into the origin. 272## if theta was 0 degrees and r was 1, then we'd be pushed away from the origin, etc... [br] 273## i can draw some pictures of the force fields this generates if you care, but as long as they feel mostly 274## ok to play with, then we can leave this be mostly 275func force(x: Vector2, f: Vector4): 276 var o = Vector2(f.x,f.y) 277 var r = f.z 278 var theta = f.w 279 var dist = tDist(x, o) 280 return dist.normalized().rotated(theta)*r/pow(dist.length()+10,FORCE_POWER) 281 282## forces is the sum of all forces evaluated at [param x] 283func forces(x: Vector2): 284 var res = Vector2.ZERO 285 for f in ff: 286 res += force(x, f) 287 return res 288 289## tDist is the toroidal distance within BOUNDS. this is some magic formula i found more on the internet, 290## the basic idea is that since we want the left side to wrap around smoothly to the right side, and 291## the top to wrap around smoothly to the bottom, instead of taking place in a rectangle, we take place 292## in an implicit torus. points that are close to the left and right side of the screen are right next 293## to each other, even though if we calculate the normal rectangular distance, then they'd be far apart. 294## to get the proper distance, we need to find the minimum distance in the x and y directions from [param from] 295## to [param to] where [param to] is in the same x screen as [param from], in the screen 1 to left of [param from], and in the screen 1 to 296## the right of [param from], and the same vertically. calculating the distances in this manner prevents a bunch 297## of ugly if statements. more reading below [br] 298## [url]https://blog.demofox.org/2017/10/01/calculating-the-distance-between-points-in-wrap-around-toroidal-space/[/url] 299func tDist(from: Vector2, to: Vector2): 300 var dx = to.x - from.x 301 if abs(dx) > abs(dx - BOUNDS.size.x): 302 dx = dx - BOUNDS.size.x 303 elif abs(dx) > abs(dx + BOUNDS.size.x): 304 dx = dx + BOUNDS.size.x 305 var dy = to.y - from.y 306 if abs(dy) > abs(dy - BOUNDS.size.y): 307 dy = dy - BOUNDS.size.y 308 elif abs(dy) > abs(dy + BOUNDS.size.y): 309 dy = dy + BOUNDS.size.y 310 return Vector2(dx,dy) 311 312func updatePositions(delta: float): 313 var remaining: float = delta 314 while remaining > 0: 315 var pcollisiont: Array[float] = [] 316 var pcollisions: Array[Vector2i] = [] 317 for i in range(stones.size()): 318 for j in range(i + 1,stones.size()): 319 # https://stackoverflow.com/questions/18410937/resolving-a-circle-circle-collision 320 var p = tDist(stones[i].position, stones[j].position) 321 if p.length() < 22.0: 322 var res = tCollision(stones[i].position, stones[j].position, stones[i].velocity, stones[j].velocity) 323 stones[i].velocity = -Vector2(res.x, res.y) 324 stones[j].velocity = -Vector2(res.z, res.w) 325 continue 326 var v = stones[i].velocity - stones[j].velocity 327 var a = v.dot(v) 328 var b = 2 * p.dot(v) 329 var c = p.dot(p) - 22.0*22.0 330 var discriminant = b*b - 4 * a * c 331 if discriminant < 0: 332 continue 333 var time1 = (-b - sqrt(discriminant)) / (2 * a) 334 var time2 = (-b + sqrt(discriminant)) / (2 * a) 335 var time = time1 336 if time1 < 0: 337 time = time2 338 if time < 0: 339 continue 340 if time > remaining: 341 continue 342 else: 343 pcollisiont.append(time) 344 pcollisions.append(Vector2i(i,j)) 345 var least = remaining 346 var leasti = -1 347 var leastj = -1 348 for i in range(pcollisions.size()): 349 if pcollisiont[i] < least: 350 least = pcollisiont[i] 351 leasti = pcollisions[i].x 352 leastj = pcollisions[i].y 353 remaining -= least 354 for i in range(stones.size()): 355 stones[i].position = tNorm(stones[i].position + stones[i].velocity*least) 356 if leasti > -1: 357 var res = tCollision(stones[leasti].position, stones[leastj].position, stones[leasti].velocity, stones[leastj].velocity) 358 stones[leasti].velocity = -Vector2(res.x, res.y) 359 stones[leastj].velocity = -Vector2(res.z, res.w) 360 361 #for i in range(stones.size()): 362 #for j in range(i + 1, stones.size()): 363 #var d = tDist(stones[i].position, stones[j].position) 364 #if d.length() < 21.0: 365 #var bump = (22.5 - d.length()) / 2 366 #stones[i].position = stones[i].position - bump * d.normalized() 367 #stones[j].position = stones[j].position + bump * d.normalized() 368 369 370func tCollision(apos: Vector2, bpos: Vector2, avel: Vector2, bvel: Vector2): 371 var d = tDist(apos, bpos) 372 var n = d.normalized() 373 var vrel = avel - bvel 374 var vn = vrel.dot(n) 375 if vn > 0: 376 return Vector4(avel.x, avel.y, bvel.x, bvel.y) 377 var avelf = avel - vn * n 378 var bvelf = bvel + vn * n 379 return Vector4(avelf.x, avelf.y, bvelf.x, bvelf.y) 380 381## tNorm is a bad name which takes [param p] and puts it within bounds, used whenever we move points and might cause 382## them to wrap around. might not be necessary for things which we don't render, but i think tDist requires 383## that both points are in the main universe so you probably should use me 384func tNorm(p: Vector2) -> Vector2: 385 var x = p.x 386 while x < 0: 387 x += BOUNDS.size.x 388 while x > BOUNDS.size.x: 389 x -= BOUNDS.size.x 390 var y = p.y 391 while y < 0: 392 y += BOUNDS.size.y 393 while y > BOUNDS.size.y: 394 y -= BOUNDS.size.y 395 return Vector2(x,y) 396 397func tNormConnected(u: Vector2, v: Vector2) -> bool: 398 return abs(tDist(u, v).length_squared() - (v - u).length_squared()) < 10 399 400 401func do_debug_inputs(delta: float): 402 if Input.is_action_just_pressed("click"): 403 #$TitleScreen.hide() 404 pass 405 if Input.is_action_just_pressed("debug"): 406 debug = !debug 407 hidedebug = Input.is_action_pressed("hide") 408 showdebug = Input.is_action_pressed("show") 409 if Input.is_action_just_pressed("regenerate"): 410 genfields() 411 rendervecfield() 412 if Input.is_action_pressed("fields_up"): 413 N_FIELDS += 1 414 if Input.is_action_pressed("fields_down"): 415 N_FIELDS -= 1 416 if Input.is_action_just_pressed("avg_up"): 417 last_avg = FORCE_AVG 418 if Input.is_action_just_pressed("avg_down"): 419 last_avg = FORCE_AVG 420 if Input.is_action_pressed("avg_up"): 421 FORCE_AVG *= 1.005 422 if Input.is_action_pressed("avg_down"): 423 FORCE_AVG *= .995 424 if Input.is_action_just_released("avg_up") || Input.is_action_just_released("avg_down"): 425 var del = FORCE_AVG - last_avg 426 for i in range(ff.size()): 427 var f = ff[i] 428 f.z = f.z + del 429 ff[i] = f 430 rendervecfield() 431 if Input.is_action_just_pressed("dev_up"): 432 last_dev = FORCE_DEV 433 if Input.is_action_just_pressed("dev_down"): 434 last_dev = FORCE_DEV 435 if Input.is_action_pressed("dev_up"): 436 FORCE_DEV *= 1.005 437 if Input.is_action_pressed("dev_down"): 438 FORCE_DEV *= .995 439 if Input.is_action_just_released("dev_up") || Input.is_action_just_released("dev_down"): 440 var ratio = FORCE_DEV / last_dev 441 for i in range(ff.size()): 442 var f = ff[i] 443 f.z = FORCE_AVG + (f.z-FORCE_AVG) * ratio 444 ff[i] = f 445 rendervecfield() 446 if Input.is_action_pressed("friction_up"): 447 MY_FRICTION *= .99999 448 if Input.is_action_pressed("friction_down"): 449 MY_FRICTION = lerp(MY_FRICTION, 1.0, delta) 450 if Input.is_action_pressed("power_up"): 451 FORCE_POWER *= 1.0005 452 if Input.is_action_pressed("power_down"): 453 FORCE_POWER *= .9995 454 if Input.is_action_just_released("power_up"): 455 rendervecfield() 456 if Input.is_action_just_released("power_down"): 457 rendervecfield() 458