summaryrefslogtreecommitdiffhomepage
path: root/samples/04_physics_and_collisions/10_collision_with_object_removal
diff options
context:
space:
mode:
authorAmir Rajan <[email protected]>2021-01-18 12:08:34 -0600
committerAmir Rajan <[email protected]>2021-01-18 12:08:34 -0600
commita4b9c048a1d751f5226833bb0c527ba1a8ac5d09 (patch)
tree3f2535e7a6272e796d50e7f07c906d4c9eb1b14a /samples/04_physics_and_collisions/10_collision_with_object_removal
parenta24a71805b1924ae7f80776c736f94575c171d2c (diff)
downloaddragonruby-game-toolkit-contrib-a4b9c048a1d751f5226833bb0c527ba1a8ac5d09.tar.gz
dragonruby-game-toolkit-contrib-a4b9c048a1d751f5226833bb0c527ba1a8ac5d09.zip
Synced with 2.3.
Diffstat (limited to 'samples/04_physics_and_collisions/10_collision_with_object_removal')
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb31
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb166
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb189
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb53
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb29
-rw-r--r--samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb50
6 files changed, 518 insertions, 0 deletions
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb
new file mode 100644
index 0000000..bdfe153
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/ball.rb
@@ -0,0 +1,31 @@
+class Ball
+ #TODO limit accessors?
+ attr_accessor :xy, :width, :height, :velocity
+
+
+ #@xy [Vector2d] x,y position
+ #@velocity [Vector2d] velocity of ball
+ def initialize
+ @xy = Vector2d.new(WIDTH/2,500)
+ @velocity = Vector2d.new(4,-4)
+ @width = 20
+ @height = 20
+ end
+
+ #move the ball according to its velocity
+ def update args
+ end
+
+ #render the ball to the screen
+ def render args
+ args.outputs.solids << [@xy.x,@xy.y,@width,@height,255,0,255];
+ #args.outputs.labels << [20,HEIGHT-50,"velocity: " [email protected]_s+","[email protected]_s + " magnitude:" + @velocity.mag.to_s]
+ end
+
+ def rect
+ [@xy.x,@xy.y,@width,@height]
+ end
+
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb
new file mode 100644
index 0000000..69ada5b
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/linear_collider.rb
@@ -0,0 +1,166 @@
+#The LinearCollider (theoretically) produces collisions upon a line segment defined point.y two x,y cordinates
+
+class LinearCollider
+
+ #start [Array of length 2] start of the line segment as a x,y cordinate
+ #last [Array of length 2] end of the line segment as a x,y cordinate
+
+ #inorder for the LinearCollider to be functional the line segment must be said to have a thickness
+ #(as it is unlikly that a colliding object will land exactly on the linesegment)
+
+ #extension defines if the line's thickness extends negatively or positively
+ #extension :pos extends positively
+ #extension :neg extends negatively
+
+ #thickness [float] how thick the line should be (should always be atleast as large as the magnitude of the colliding object)
+ def initialize (pointA, pointB, extension=:neg, thickness=10)
+ @pointA = pointA
+ @pointB = pointB
+ @thickness = thickness
+ @extension = extension
+
+ @pointAExtended={
+ x: @pointA.x + @thickness*(@extension == :neg ? -1 : 1),
+ y: @pointA.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+ @pointBExtended={
+ x: @pointB.x + @thickness*(@extension == :neg ? -1 : 1),
+ y: @pointB.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+
+ end
+
+ def resetPoints(pointA,pointB)
+ @pointA = pointA
+ @pointB = pointB
+
+ @pointAExtended={
+ x:@pointA.x + @thickness*(@extension == :neg ? -1 : 1),
+ y:@pointA.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+ @pointBExtended={
+ x:@pointB.x + @thickness*(@extension == :neg ? -1 : 1),
+ y:@pointB.y + @thickness*(@extension == :neg ? -1 : 1)
+ }
+ end
+
+ #TODO: Ugly function
+ def slope (pointA, pointB)
+ return (pointB.x==pointA.x) ? INFINITY : (pointB.y+-pointA.y)/(pointB.x+-pointA.x)
+ end
+
+ #TODO: Ugly function
+ def intercept(pointA, pointB)
+ if (slope(pointA, pointB) == INFINITY)
+ -INFINITY
+ elsif slope(pointA, pointB) == -1*INFINITY
+ INFINITY
+ else
+ pointA.y+-1.0*(slope(pointA, pointB)*pointA.x)
+ end
+ end
+
+ def calcY(pointA, pointB, x)
+ return slope(pointA, pointB)*x + intercept(pointA, pointB)
+ end
+
+ #test if a collision has occurred
+ def isCollision? (point)
+ #INFINITY slop breaks down when trying to determin collision, ergo it requires a special test
+ if slope(@pointA, @pointB) == INFINITY &&
+ point.x >= [@pointA.x,@pointB.x].min+(@extension == :pos ? -@thickness : 0) &&
+ point.x <= [@pointA.x,@pointB.x].max+(@extension == :neg ? @thickness : 0) &&
+ point.y >= [@pointA.y,@pointB.y].min && point.y <= [@pointA.y,@pointB.y].max
+ return true
+ end
+
+ isNegInLine = @extension == :neg &&
+ point.y <= slope(@pointA, @pointB)*point.x+intercept(@pointA,@pointB) &&
+ point.y >= point.x*slope(@pointAExtended, @pointBExtended)+intercept(@pointAExtended,@pointBExtended)
+ isPosInLine = @extension == :pos &&
+ point.y >= slope(@pointA, @pointB)*point.x+intercept(@pointA,@pointB) &&
+ point.y <= point.x*slope(@pointAExtended, @pointBExtended)+intercept(@pointAExtended,@pointBExtended)
+ isInBoxBounds = point.x >= [@pointA.x,@pointB.x].min &&
+ point.x <= [@pointA.x,@pointB.x].max &&
+ point.y >= [@pointA.y,@pointB.y].min+(@extension == :neg ? -@thickness : 0) &&
+ point.y <= [@pointA.y,@pointB.y].max+(@extension == :pos ? @thickness : 0)
+
+ return isInBoxBounds && (isNegInLine || isPosInLine)
+
+ end
+
+ def getRepelMagnitude (fbx, fby, vrx, vry, args)
+ a = fbx ; b = vrx ; c = fby
+ d = vry ; e = args.state.ball.velocity.mag
+
+ if b**2 + d**2 == 0
+ puts "magnitude error"
+ end
+
+ x1 = (-a*b+-c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 + d**2 - a**2 * d**2)**0.5)/(b**2 + d**2)
+ x2 = -((a*b + c*d + (e**2 * b**2 - b**2 * c**2 + 2*a*b*c*d + e**2 * d**2 - a**2 * d**2)**0.5)/(b**2 + d**2))
+ return ((a+x1*b)**2 + (c+x1*d)**2 == e**2) ? x1 : x2
+ end
+
+ def update args
+ #each of the four points on the square ball - NOTE simple to extend to a circle
+ points= [ {x: args.state.ball.xy.x, y: args.state.ball.xy.y},
+ {x: args.state.ball.xy.x+args.state.ball.width, y: args.state.ball.xy.y},
+ {x: args.state.ball.xy.x, y: args.state.ball.xy.y+args.state.ball.height},
+ {x: args.state.ball.xy.x+args.state.ball.width, y: args.state.ball.xy.y + args.state.ball.height}
+ ]
+
+ #for each point p in points
+ for point in points
+ #isCollision.md has more information on this section
+ #TODO: section can certainly be simplifyed
+ if isCollision?(point)
+ u = Vector2d.new(1.0,((slope(@pointA, @pointB)==0) ? INFINITY : -1/slope(@pointA, @pointB))*1.0).normalize #normal perpendicular (to line segment) vector
+
+ #the vector with the repeling force can be u or -u depending of where the ball was coming from in relation to the line segment
+ previousBallPosition=Vector2d.new(point.x-args.state.ball.velocity.x,point.y-args.state.ball.velocity.y)
+ choiceA = (u.mult(1))
+ choiceB = (u.mult(-1))
+ vectorRepel = nil
+
+ if (slope(@pointA, @pointB))!=INFINITY && u.y < 0
+ choiceA, choiceB = choiceB, choiceA
+ end
+ vectorRepel = (previousBallPosition.y > calcY(@pointA, @pointB, previousBallPosition.x)) ? choiceA : choiceB
+
+ #vectorRepel = (previousBallPosition.y > slope(@pointA, @pointB)*previousBallPosition.x+intercept(@pointA,@pointB)) ? choiceA : choiceB)
+ if (slope(@pointA, @pointB) == INFINITY) #slope INFINITY breaks down in the above test, ergo it requires a custom test
+ vectorRepel = (previousBallPosition.x > @pointA.x) ? (u.mult(1)) : (u.mult(-1))
+ end
+ #puts (" " + $t[0].to_s + "," + $t[1].to_s + " " + $t[2].to_s + "," + $t[3].to_s + " " + " " + u.x.to_s + "," + u.y.to_s)
+ #vectorRepel now has the repeling force
+
+ mag = args.state.ball.velocity.mag
+ theta_ball=Math.atan2(args.state.ball.velocity.y,args.state.ball.velocity.x) #the angle of the ball's velocity
+ theta_repel=Math.atan2(vectorRepel.y,vectorRepel.x) #the angle of the repeling force
+ #puts ("theta:" + theta_ball.to_s + " " + theta_repel.to_s) #theta okay
+
+ fbx = mag * Math.cos(theta_ball) #the x component of the ball's velocity
+ fby = mag * Math.sin(theta_ball) #the y component of the ball's velocity
+
+ repelMag = getRepelMagnitude(fbx, fby, vectorRepel.x, vectorRepel.y, args)
+
+ frx = repelMag* Math.cos(theta_repel) #the x component of the repel's velocity | magnitude is set to twice of fbx
+ fry = repelMag* Math.sin(theta_repel) #the y component of the repel's velocity | magnitude is set to twice of fby
+
+ fsumx = fbx+frx #sum of x forces
+ fsumy = fby+fry #sum of y forces
+ fr = mag#fr is the resulting magnitude
+ thetaNew = Math.atan2(fsumy, fsumx) #thetaNew is the resulting angle
+ xnew = fr*Math.cos(thetaNew) #resulting x velocity
+ ynew = fr*Math.sin(thetaNew) #resulting y velocity
+
+ args.state.ball.velocity = Vector2d.new(xnew,ynew)
+ #args.state.ball.xy.add(args.state.ball.velocity)
+ break #no need to check the other points ?
+ else
+ end
+ end
+ end #end update
+
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb
new file mode 100644
index 0000000..adde51f
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/main.rb
@@ -0,0 +1,189 @@
+# coding: utf-8
+INFINITY= 10**10
+WIDTH=1280
+HEIGHT=720
+
+require 'app/vector2d.rb'
+require 'app/paddle.rb'
+require 'app/ball.rb'
+require 'app/linear_collider.rb'
+
+#Method to init default values
+def defaults args
+ args.state.game_board ||= [(args.grid.w / 2 - args.grid.w / 4), 0, (args.grid.w / 2), args.grid.h]
+ args.state.bricks ||= []
+ args.state.num_bricks ||= 0
+ args.state.game_over_at ||= 0
+ args.state.paddle ||= Paddle.new
+ args.state.ball ||= Ball.new
+ args.state.westWall ||= LinearCollider.new({x: args.grid.w/4, y: 0}, {x: args.grid.w/4, y: args.grid.h}, :pos)
+ args.state.eastWall ||= LinearCollider.new({x: 3*args.grid.w*0.25, y: 0}, {x: 3*args.grid.w*0.25, y: args.grid.h})
+ args.state.southWall ||= LinearCollider.new({x: 0, y: 0}, {x: args.grid.w, y: 0})
+ args.state.northWall ||= LinearCollider.new({x: 0, y:args.grid.h}, {x: args.grid.w, y: args.grid.h}, :pos)
+
+ #args.state.testWall ||= LinearCollider.new({x:0 , y:0},{x:args.grid.w, y:args.grid.h})
+end
+
+#Render loop
+def render args
+ render_instructions args
+ render_board args
+ render_bricks args
+end
+
+begin :render_methods
+ #Method to display the instructions of the game
+ def render_instructions args
+ args.outputs.labels << [225, args.grid.h - 30, "← and → to move the paddle left and right", 0, 1]
+ end
+
+ def render_board args
+ args.outputs.borders << args.state.game_board
+ end
+
+ def render_bricks args
+ args.outputs.solids << args.state.bricks.map(&:rect)
+ end
+end
+
+#Calls all methods necessary for performing calculations
+def calc args
+ add_new_bricks args
+ reset_game args
+ calc_collision args
+ win_game args
+
+ args.state.westWall.update args
+ args.state.eastWall.update args
+ args.state.southWall.update args
+ args.state.northWall.update args
+ args.state.paddle.update args
+ args.state.ball.update args
+
+ #args.state.testWall.update args
+
+ args.state.paddle.render args
+ args.state.ball.render args
+end
+
+begin :calc_methods
+ def add_new_bricks args
+ return if args.state.num_bricks > 40
+
+ #Width of the game board is 640px
+ brick_width = (args.grid.w / 2) / 10
+ brick_height = brick_width / 2
+
+ (4).map_with_index do |y|
+ #Make a box that is 10 bricks wide and 4 bricks tall
+ args.state.bricks += (10).map_with_index do |x|
+ args.state.new_entity(:brick) do |b|
+ b.x = x * brick_width + (args.grid.w / 2 - args.grid.w / 4)
+ b.y = args.grid.h - ((y + 1) * brick_height)
+ b.rect = [b.x + 1, b.y - 1, brick_width - 2, brick_height - 2, 235, 50 * y, 52]
+
+ #Add linear colliders to the brick
+ b.collider_bottom = LinearCollider.new([(b.x-2), (b.y-5)], [(b.x+brick_width+1), (b.y-5)], :pos, brick_height)
+ b.collider_right = LinearCollider.new([(b.x+brick_width+1), (b.y-5)], [(b.x+brick_width+1), (b.y+brick_height+1)], :pos)
+ b.collider_left = LinearCollider.new([(b.x-2), (b.y-5)], [(b.x-2), (b.y+brick_height+1)], :neg)
+ b.collider_top = LinearCollider.new([(b.x-2), (b.y+brick_height+1)], [(b.x+brick_width+1), (b.y+brick_height+1)], :neg)
+
+ # @xyCollision = LinearCollider.new({x: @x,y: @y+@height}, {x: @x+@width, y: @y+@height})
+ # @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos)
+ # @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height})
+ # @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height}, :pos)
+
+ b.broken = false
+
+ args.state.num_bricks += 1
+ end
+ end
+ end
+ end
+
+ def reset_game args
+ if args.state.ball.xy.y < 20 && args.state.game_over_at.elapsed_time > 60
+ #Freeze the ball
+ args.state.ball.velocity.x = 0
+ args.state.ball.velocity.y = 0
+ #Freeze the paddle
+ args.state.paddle.enabled = false
+
+ args.state.game_over_at = args.state.tick_count
+ end
+
+ if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count != 0
+ #Display a "Game over" message
+ args.outputs.labels << [100, 100, "GAME OVER", 10]
+ end
+
+ #If 60 frames have passed since the game ended, restart the game
+ if args.state.game_over_at != 0 && args.state.game_over_at.elapsed_time == 60
+ # FIXME: only put value types in state
+ args.state.ball = Ball.new
+
+ # FIXME: only put value types in state
+ args.state.paddle = Paddle.new
+
+ args.state.bricks = []
+ args.state.num_bricks = 0
+ end
+ end
+
+ def calc_collision args
+ #Remove the brick if it is hit with the ball
+ ball = args.state.ball
+ ball_rect = [ball.xy.x, ball.xy.y, 20, 20]
+
+ #Loop through each brick to see if the ball is colliding with it
+ args.state.bricks.each do |b|
+ if b.rect.intersect_rect?(ball_rect)
+ #Run the linear collider for the brick if there is a collision
+ b[:collider_bottom].update args
+ b[:collider_right].update args
+ b[:collider_left].update args
+ b[:collider_top].update args
+
+ b.broken = true
+ end
+ end
+
+ args.state.bricks = args.state.bricks.reject(&:broken)
+ end
+
+ def win_game args
+ if args.state.bricks.count == 0 && args.state.game_over_at.elapsed_time > 60
+ #Freeze the ball
+ args.state.ball.velocity.x = 0
+ args.state.ball.velocity.y = 0
+ #Freeze the paddle
+ args.state.paddle.enabled = false
+
+ args.state.game_over_at = args.state.tick_count
+ end
+
+ if args.state.game_over_at.elapsed_time < 60 && args.state.tick_count > 60 && args.state.bricks.count == 0
+ #Display a "Game over" message
+ args.outputs.labels << [100, 100, "CONGRATULATIONS!", 10]
+ end
+ end
+
+end
+
+def tick args
+ defaults args
+ render args
+ calc args
+
+ #args.outputs.lines << [0, 0, args.grid.w, args.grid.h]
+
+ #$tc+=1
+ #if $tc == 5
+ #$train << [args.state.ball.xy.x, args.state.ball.xy.y]
+ #$tc = 0
+ #end
+ #for t in $train
+
+ #args.outputs.solids << [t[0],t[1],5,5,255,0,0];
+ #end
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb
new file mode 100644
index 0000000..a4fe710
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/paddle.rb
@@ -0,0 +1,53 @@
+class Paddle
+ attr_accessor :enabled
+
+ def initialize ()
+ @x=WIDTH/2
+ @y=100
+ @width=100
+ @height=20
+ @speed=10
+
+ @xyCollision = LinearCollider.new({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5})
+ @xyCollision2 = LinearCollider.new({x: @x,y: @y}, {x: @x+@width, y: @y}, :pos)
+ @xyCollision3 = LinearCollider.new({x: @x,y: @y}, {x: @x, y: @y+@height+5})
+ @xyCollision4 = LinearCollider.new({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5}, :pos)
+
+ @enabled = true
+ end
+
+ def update args
+ @xyCollision.resetPoints({x: @x,y: @y+@height+5}, {x: @x+@width, y: @y+@height+5})
+ @xyCollision2.resetPoints({x: @x,y: @y}, {x: @x+@width, y: @y})
+ @xyCollision3.resetPoints({x: @x,y: @y}, {x: @x, y: @y+@height+5})
+ @xyCollision4.resetPoints({x: @x+@width,y: @y}, {x: @x+@width, y: @y+@height+5})
+
+ @xyCollision.update args
+ @xyCollision2.update args
+ @xyCollision3.update args
+ @xyCollision4.update args
+
+ args.inputs.keyboard.key_held.left ||= false
+ args.inputs.keyboard.key_held.right ||= false
+
+ if not (args.inputs.keyboard.key_held.left == args.inputs.keyboard.key_held.right)
+ if args.inputs.keyboard.key_held.left && @enabled
+ @x-=@speed
+ elsif args.inputs.keyboard.key_held.right && @enabled
+ @x+=@speed
+ end
+ end
+
+ xmin =WIDTH/4
+ xmax = 3*(WIDTH/4)
+ @x = (@x+@width > xmax) ? xmax-@width : (@x<xmin) ? xmin : @x;
+ end
+
+ def render args
+ args.outputs.solids << [@x,@y,@width,@height,255,0,0];
+ end
+
+ def rect
+ [@x, @y, @width, @height]
+ end
+end
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb
new file mode 100644
index 0000000..db71ff6
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/tests.rb
@@ -0,0 +1,29 @@
+# For advanced users:
+# You can put some quick verification tests here, any method
+# that starts with the `test_` will be run when you save this file.
+
+# Here is an example test and game
+
+# To run the test: ./dragonruby mygame --eval app/tests.rb --no-tick
+
+class MySuperHappyFunGame
+ attr_gtk
+
+ def tick
+ outputs.solids << [100, 100, 300, 300]
+ end
+end
+
+def test_universe args, assert
+ game = MySuperHappyFunGame.new
+ game.args = args
+ game.tick
+ assert.true! args.outputs.solids.length == 1, "failure: a solid was not added after tick"
+ assert.false! 1 == 2, "failure: some how, 1 equals 2, the world is ending"
+ puts "test_universe completed successfully"
+end
+
+puts "running tests"
+$gtk.reset 100
+$gtk.log_level = :off
+$gtk.tests.start
diff --git a/samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb
new file mode 100644
index 0000000..97cf286
--- /dev/null
+++ b/samples/04_physics_and_collisions/10_collision_with_object_removal/app/vector2d.rb
@@ -0,0 +1,50 @@
+
+class Vector2d
+ attr_accessor :x, :y
+
+ def initialize x=0, y=0
+ @x=x
+ @y=y
+ end
+
+ #returns a vector multiplied by scalar x
+ #x [float] scalar
+ def mult x
+ r = Vector2d.new(0,0)
+ r.x=@x*x
+ r.y=@y*x
+ r
+ end
+
+ # vect [Vector2d] vector to copy
+ def copy vect
+ Vector2d.new(@x, @y)
+ end
+
+ #returns a new vector equivalent to this+vect
+ #vect [Vector2d] vector to add to self
+ def add vect
+ Vector2d.new(@x+vect.x,@y+vect.y)
+ end
+
+ #returns a new vector equivalent to this-vect
+ #vect [Vector2d] vector to subtract to self
+ def sub vect
+ Vector2d.new(@x-vect.c, @y-vect.y)
+ end
+
+ #return the magnitude of the vector
+ def mag
+ ((@x**2)+(@y**2))**0.5
+ end
+
+ #returns a new normalize version of the vector
+ def normalize
+ Vector2d.new(@x/mag, @y/mag)
+ end
+
+ #TODO delet?
+ def distABS vect
+ (((vect.x-@x)**2+(vect.y-@y)**2)**0.5).abs()
+ end
+end