diff options
| author | Amir Rajan <[email protected]> | 2021-01-18 12:08:34 -0600 |
|---|---|---|
| committer | Amir Rajan <[email protected]> | 2021-01-18 12:08:34 -0600 |
| commit | a4b9c048a1d751f5226833bb0c527ba1a8ac5d09 (patch) | |
| tree | 3f2535e7a6272e796d50e7f07c906d4c9eb1b14a /samples/04_physics_and_collisions/10_collision_with_object_removal | |
| parent | a24a71805b1924ae7f80776c736f94575c171d2c (diff) | |
| download | dragonruby-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')
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 |
