technohippy-Pongo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ module Pongo
2
+ # The abstract base class for all particles.
3
+ #
4
+ # You should not instantiate this class directly -- instead use one of the subclasses.
5
+ class AbstractParticle < AbstractItem
6
+ attr_accessor :curr, :prev, :samp, :interval, :temp, :forces, :force_list, :collision, :first_collision
7
+ attr_accessor :kfr, :mass, :inv_mass, :friction, :fixed, :collidable, :center, :multisample
8
+
9
+ def initialize(x, y, is_fixed, mass, elasticity, friction)
10
+ super()
11
+ @interval = Interval.new
12
+ @curr = Vector.new(x, y)
13
+ @prev = Vector.new(x, y)
14
+ @samp = Vector.new
15
+ @temp = Vector.new
16
+ @fixed = is_fixed
17
+ @forces = Vector.new
18
+ @force_list = []
19
+ @collision = Collision.new
20
+ @collidable = true
21
+ @first_collision = false
22
+ self.mass = mass
23
+ self.elasticity = elasticity
24
+ self.friction = friction
25
+ set_style
26
+ @center = Vector.new
27
+ @multisample = 0
28
+ end
29
+
30
+ def mass=(m)
31
+ raise ArgumentError.new('mass may not be set <= 0') if m <= 0
32
+ @mass = m
33
+ @inv_mass = 1.0 / m
34
+ end
35
+
36
+ # The elasticity of the particle. Standard values are between 0 and 1.
37
+ # The higher the value, the greater the elasticity.
38
+ #
39
+ # During collisions the elasticity values are combined. If one particle's
40
+ # elasticity is set to 0.4 and the other is set to 0.4 then the collision will
41
+ # be have a total elasticity of 0.8. The result will be the same if one particle
42
+ # has an elasticity of 0 and the other 0.8.
43
+ #
44
+ # Setting the elasticity to greater than 1 (of a single particle, or in a combined
45
+ # collision) will cause particles to bounce with energy greater than naturally
46
+ # possible.
47
+ def elasticity
48
+ @kfr
49
+ end
50
+
51
+ def elasticity=(k)
52
+ @kfr = k
53
+ end
54
+
55
+ def center
56
+ @center.set_to(px, py)
57
+ @center
58
+ end
59
+
60
+ # The surface friction of the particle. Values must be in the range of 0 to 1.
61
+ #
62
+ # 0 is no friction (slippery), 1 is full friction (sticky).
63
+ #
64
+ # During collisions, the friction values are summed, but are clamped between 1
65
+ # and 0. For example, If two particles have 0.7 as their surface friction, then
66
+ # the resulting friction between the two particles will be 1 (full friction).
67
+ #
68
+ # There is a bug in the current release where colliding non-fixed particles with
69
+ # friction greater than 0 will behave erratically. A workaround is to only set
70
+ # the friction of fixed particles.
71
+ def friction=(f)
72
+ raise ArgumentError.new('Legal friction must be >= 0 and <= 1') if f < 0 or 1 < f
73
+ @friction = f
74
+ end
75
+
76
+ def fixed?
77
+ @fixed
78
+ end
79
+
80
+ # The position of the particle. Getting the position of the particle is useful
81
+ # for drawing it or testing it for some custom purpose.
82
+ #
83
+ # <p>
84
+ # When you get the <code>position</code> of a particle you are given a copy of
85
+ # the current location. Because of this you cannot change the position of a
86
+ # particle by altering the <code>x</code> and <code>y</code> components of the
87
+ # Vector you have retrieved from the position property. You have to do something
88
+ # instead like: <code> position = new Vector(100,100)</code>, or you can use the
89
+ # <code>px</code> and <code>py</code> properties instead.
90
+ # </p>
91
+ #
92
+ # <p>
93
+ # You can alter the position of a particle three ways: change its position, set
94
+ # its velocity, or apply a force to it. Setting the position of a non-fixed
95
+ # particle is not the same as setting its fixed property to true. A particle held
96
+ # in place by its position will behave as if it's attached there by a 0 length
97
+ # spring constraint.
98
+ def position
99
+ @curr.dup
100
+ end
101
+
102
+ def position=(p)
103
+ @curr.copy(p)
104
+ @prev.copy(p)
105
+ end
106
+
107
+ def px
108
+ @curr.x
109
+ end
110
+
111
+ def px=(x)
112
+ @curr.x = x
113
+ @prev.x = x
114
+ end
115
+
116
+ def py
117
+ @curr.y
118
+ end
119
+
120
+ def py=(y)
121
+ @curr.y = y
122
+ @prev.y = y
123
+ end
124
+
125
+ # The velocity of the particle. If you need to change the motion of a particle,
126
+ # you should either use this property, or one of the addForce methods. Generally,
127
+ # the addForce methods are best for slowly altering the motion. The velocity
128
+ # property is good for instantaneously setting the velocity, e.g., for
129
+ # projectiles.
130
+ def velocity
131
+ @curr - @prev
132
+ end
133
+
134
+ def velocity=(v)
135
+ @prev = @curr - v
136
+ end
137
+
138
+ # Determines if the particle can collide with other particles or constraints.
139
+ # The default state is true.
140
+ def collidable?
141
+ @collidable
142
+ end
143
+
144
+ def collidable!
145
+ @collidable = true
146
+ end
147
+
148
+ # Adds a force to the particle. Using this method to a force directly to the
149
+ # particle will only apply that force for a single APEngine.step() cycle.
150
+ def add_force(force)
151
+ @force_list << force
152
+ end
153
+
154
+ # The <code>update()</code> method is called automatically during the
155
+ # APEngine.step() cycle. This method integrates the particle.
156
+ def update(dt2)
157
+ return if fixed?
158
+
159
+ accumulate_forces
160
+
161
+ @temp.copy(@curr)
162
+ nv = velocity + @forces.mult!(dt2)
163
+ @curr.plus!(nv.mult!(APEngine.damping))
164
+ @prev.copy(@temp)
165
+
166
+ clear_forces
167
+ end
168
+
169
+ # Resets the collision state of the particle. This value is used in conjuction
170
+ # with the CollisionEvent.FIRST_COLLISION event.
171
+ def reset_first_collision!
172
+ @first_collision = false
173
+ end
174
+ alias reset_first_collision reset_first_collision!
175
+
176
+ def components(collision_normal)
177
+ vel = velocity
178
+ vdotn = collision_normal.dot(vel)
179
+ @collision.vn = collision_normal * vdotn
180
+ @collision.vt = vel - collision.vn
181
+ @collision
182
+ end
183
+ alias get_components components
184
+
185
+ # Make sure to align the overriden versions of this method in
186
+ # WheelParticle
187
+ def resolve_collision(mtd, vel, n, d, order, particle)
188
+ test_particle_events(particle)
189
+ return if fixed? or (not solid?) or (not particle.solid?)
190
+ @curr.copy(@samp)
191
+ @curr.plus!(mtd)
192
+ self.velocity = vel
193
+ end
194
+
195
+ def test_particle_events(p)
196
+ if has_event_listener(CollisionEvent::COLLIDE)
197
+ dispatch_event(CollisionEvent.new(CollisionEvent::COLLIDE, p))
198
+ end
199
+ if has_event_listener(CollisionEvent::FIRST_COLLIDE) and not @first_collision
200
+ @first_collision = true
201
+ dispatch_event(CollisionEvent.new(CollisionEvent::FIRST_COLLIDE, p))
202
+ end
203
+ end
204
+
205
+ def inv_mass
206
+ fixed? ? 0 : @inv_mass
207
+ end
208
+
209
+ # Accumulates both the particle forces and the global forces
210
+ def accumulate_forces
211
+ @force_list.each {|f| @forces.plus!(f.get_value(@inv_mass))}
212
+ APEngine.forces.each {|f| @forces.plus!(f.get_value(@inv_mass))}
213
+ @forces
214
+ end
215
+
216
+ # Clears out all forces on the particle
217
+ def clear_forces
218
+ @force_list.clear
219
+ @forces.set_to(0, 0)
220
+ end
221
+
222
+ def solid?
223
+ @solid
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,108 @@
1
+ require 'pongo/spring_constraint'
2
+
3
+ module Pongo
4
+ # An Angular Constraint between 3 particles
5
+ class AngularConstraint < SpringConstraint
6
+ attr_accessor :p3, :min_ang, :max_ang, :min_break_ang, :max_break_ang
7
+
8
+ def initialize(p1, p2, p3, min_ang, max_ang, options={})
9
+ options = {:min_break_ang => -10, :max_break_ang => 10, :stiffness => 0.5,
10
+ :dependent => false, :collidable => false, :rect_height => 1, :rect_scale => 1,
11
+ :scale_to_length => false}.update(options)
12
+
13
+ super(p2, p2, stiffness, false, dependent, collidable, rect_height, rect_scale, scale_to_length)
14
+
15
+ self.p3 = p3
16
+ if min_ang == 10
17
+ self.min_ang = ac_radian
18
+ self.max_ang = ac_radian
19
+ else
20
+ self.min_ang = min_ang
21
+ self.max_ang = max_ang
22
+ end
23
+ self.min_break_ang = min_break_ang
24
+ self.max_break_ang = max_break_ang
25
+ end
26
+
27
+ # The current difference between the angle of p1, p2, and p3 and a straight line (pi)
28
+ def ac_radian
29
+ ang12 = Math.atan2(@p2.curr.y - @p1.curr.y, @p2.curr.x - @p1.curr.x)
30
+ ang23 = Math.atan2(@p3.curr.y - @p2.curr.y, @p3.curr.x - @p2.curr.x)
31
+ ang12 - ang23
32
+ end
33
+
34
+ # Returns true if the passed particle is one of the three particles attached
35
+ # to this AngularConstraint.
36
+ def is_connected_to?(p)
37
+ [@p1, @p2, @p3].include?(p)
38
+ end
39
+
40
+ # Returns true if any connected particle's <code>fixed</code> property is true.
41
+ def fixed?
42
+ @p1.fixed? and @p2.fixed? and @p3.fixed?
43
+ end
44
+ alias fixed fixed?
45
+
46
+ def resolve
47
+ return if broken
48
+
49
+ ang12 = Math.atan2(@p2.curr.y - @p1.curr.y, @p2.curr.x - @p1.curr.x)
50
+ ang23 = Math.atan2(self.p3.curr.y - @p2.curr.y, self.p3.curr.x - @p2.curr.x)
51
+
52
+ ang_diff = normalize_angle(ang12 - ang23)
53
+
54
+ p2_inv_mass = dependent ? 0 : @p2.inv_mass
55
+
56
+ sum_inv_mass = @p1.inv_mass + p2_inv_mass
57
+ mult1 = @p1.inv_mass / sum_inv_mass
58
+ mult2 = p2_inv_mass / sum_inv_mass
59
+ ang_change = 0
60
+
61
+ low_mid = (self.max_ang - self.min_ang) / 2
62
+ high_mid = (self.max_ang + self.min_ang) / 2
63
+ break_ang = (self.max_break_ang - self.min_break_ang) / 2
64
+
65
+ new_diff = normalize_angle(high_mid - ang_diff)
66
+
67
+ if new_diff > low_mid
68
+ if new_diff > break_ang
69
+ diff = new_diff - break_ang
70
+ broken = true
71
+ if has_event_listener(BreakEvent::ANGULAR)
72
+ dispatch_event(BreakEvent.new(BreakEvent::ANGULAR, diff))
73
+ end
74
+ return
75
+ end
76
+ ang_change = new_diff - low_mid
77
+ elsif new_diff < -low_mid
78
+ if new_diff < -break_ang
79
+ diff = new_dif + break_ang
80
+ broken = true
81
+ if has_event_listener(BreakEvent::ANGULAR)
82
+ dispatch_event(BreakEvent.new(BreakEvent::ANGULAR, diff))
83
+ end
84
+ return
85
+ end
86
+ ang_change = new_diff + low_mid
87
+ end
88
+
89
+ final_ang = ang_change * self.stiffness + ang12
90
+ displace_x = @p1.curr.x + (@p2.curr.x - @p1.curr.x) * mult1
91
+ displace_y = @p1.curr.y + (@p2.curr.y - @p1.curr.y) * mult1
92
+
93
+ @p1.curr.x = displace_x + Math.cos(final_ang + Math::PI) * rest_length * mult1
94
+ @p1.curr.x = displace_y + Math.sin(final_ang + Math::PI) * rest_length * mult1
95
+ @p2.curr.x = displace_x + Math.cos(final_ang) * rest_length * mult2
96
+ @p2.curr.y = displace_y + Math.sin(final_ang) * rest_length * mult2
97
+ end
98
+
99
+ protected
100
+
101
+ def normilize_angle(angle)
102
+ pi2 = Math::PI * 2
103
+ angle -= pi2 while angle > Math::PI
104
+ angle += pi2 while angle < -Math::PI
105
+ angle
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,157 @@
1
+ require 'pongo/group'
2
+ module Pongo
3
+ # The main engine class.
4
+ class PhysicsEngine
5
+ attr_accessor :forces, :groups, :tile_step, :damping, :container,
6
+ :constraint_cycles, :constraint_collision_cycles, :renderer, :logger
7
+
8
+ def container=(c)
9
+ @container = c
10
+ @renderer = c.renderer
11
+ @logger = c.logger
12
+ end
13
+
14
+ # Initializes the engine. You must call this method prior to adding any
15
+ # particles or constraints.
16
+ def setup(options={}) #dt=0.25)
17
+ options = {
18
+ :dt => 0.25, :damping => 1, :constraint_cycles => 0,
19
+ :constraint_collision_cycles => 1
20
+ }.update(options.is_a?(Numeric) ? {:dt => options} : options)
21
+
22
+ @time_step = options[:dt] * options[:dt]
23
+ @groups = []
24
+ @forces = []
25
+ self.damping = options[:damping]
26
+ self.constraint_cycles = options[:constraint_cycles]
27
+ self.constraint_collision_cycles = options[:constraint_collision_cycles]
28
+
29
+ self.container = options[:container] if options[:container]
30
+ self.renderer = options[:renderer] if options[:renderer]
31
+ self.logger = options[:logger] if options[:logger]
32
+ self.gravity = options[:gravity] if options[:gravity]
33
+ end
34
+ alias init setup
35
+
36
+ # Adds a force to all particles in the system. The forces added to the APEngine
37
+ # class are persistent - once a force is added it is continually applied each
38
+ # APEngine.step() cycle.
39
+ def add_force(force)
40
+ @forces << force
41
+ end
42
+
43
+ def gravity=(val)
44
+ case val
45
+ when Numeric
46
+ add_force(VectorForce.new(false, 0, val))
47
+ when Array
48
+ add_force(VectorForce.new(false, *val))
49
+ when VectorForce
50
+ add_force(val)
51
+ else
52
+ raise ArgumentError
53
+ end
54
+ end
55
+
56
+ # Removes a force from the engine.
57
+ def remove_force(force)
58
+ @forces.remove(force)
59
+ end
60
+
61
+ # Removes all forces from the engine.
62
+ def remove_all_forces
63
+ @forces.clear
64
+ end
65
+
66
+ def build_group(collide_internal=false)
67
+ group = Group.new(collide_internal)
68
+ @groups << group
69
+ group
70
+ end
71
+
72
+ # Adds a Group to the engine.
73
+ def add_group(group)
74
+ @groups << group
75
+ group.is_parented = true
76
+ group.init
77
+ end
78
+
79
+ # Removes a Group from the engine.
80
+ def remove_group(group)
81
+ if @groups.delete(group)
82
+ group.is_parented = false
83
+ group.cleanup
84
+ end
85
+ end
86
+
87
+ # The main step function of the engine. This method should be called
88
+ # continously to advance the simulation. The faster this method is
89
+ # called, the faster the simulation will run. Usually you would call
90
+ # this in your main program loop.
91
+ def step
92
+ integrate
93
+ constraint_cycles.times do
94
+ satisfy_constraints
95
+ end
96
+ constraint_collision_cycles.times do
97
+ satisfy_constraints
98
+ check_collisions
99
+ end
100
+ end
101
+
102
+ # Calling this method will in turn call each Group's paint() method.
103
+ # Generally you would call this method after stepping the engine in
104
+ # the main program cycle.
105
+ def draw
106
+ @groups.each {|g| g.draw}
107
+ end
108
+ alias paint draw
109
+
110
+ def integrate
111
+ @groups.each {|g| g.integrate(@time_step)}
112
+ end
113
+
114
+ def satisfy_constraints
115
+ @groups.each {|g| g.satisfy_constraints}
116
+ end
117
+
118
+ def check_collisions
119
+ @groups.each {|g| g.check_collisions}
120
+ end
121
+
122
+ def <<(item)
123
+ if item.is_a?(Group)
124
+ add_group(item)
125
+ else
126
+ if @groups.empty?
127
+ @groups << Group.new(true)
128
+ @groups.first.is_parented = true
129
+ @groups.first.init
130
+ end
131
+ @groups.last << item
132
+ end
133
+ end
134
+
135
+ def create_group(collide_internal=true, &block)
136
+ @groups << Group.new(collide_internal)
137
+ block.call(@groups.last) if block
138
+ @groups.last
139
+ end
140
+
141
+ def next_frame(options={})
142
+ options[:before].call if options[:before]
143
+ step
144
+ draw
145
+ options[:after].call if options[:after]
146
+ rescue
147
+ log($!)
148
+ raise $!
149
+ end
150
+
151
+ def log(message, level=:info)
152
+ message = message.message + "\n" + message.backtrace.join("\n") if message.is_a?(Exception)
153
+ @logger.send(level, message) if @logger
154
+ end
155
+ end
156
+ APEngine = PhysicsEngine.new
157
+ end