1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
|
# This is a simple demo for advanced collisions and mesh creation from a set
# of points. Its purpose is only to give an idea on how to make complex stuff.
# Check garden.collider for better performance.
from math import cos, sin, pi, sqrt
from random import random, randint
from itertools import combinations
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.graphics import Color, Mesh, Point
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import (
ListProperty,
StringProperty,
ObjectProperty,
NumericProperty
)
# Cloud polygon, 67 vertices + custom origin [150, 50]
cloud_poly = [
150, 50,
109.7597, 112.9600, 115.4326, 113.0853, 120.1966, 111.9883,
126.0889, 111.9570, 135.0841, 111.9570, 138.5944, 112.5525,
145.7403, 115.5301, 150.5357, 120.3256, 155.5313, 125.5938,
160.8438, 130.5000, 165.7813, 132.5000, 171.8125, 132.3438,
177.5000, 128.4688, 182.1531, 121.4990, 185.1438, 114.0406,
185.9181, 108.5649, 186.2226, 102.5978, 187.8059, 100.2231,
193.2257, 100.1622, 197.6712, 101.8671, 202.6647, 104.1809,
207.1102, 105.8858, 214.2351, 105.0333, 219.3747, 102.8301,
224.0413, 98.7589, 225.7798, 93.7272, 226.0000, 86.8750,
222.9375, 81.0625, 218.3508, 76.0867, 209.8301, 70.8090,
198.7806, 66.1360, 189.7651, 62.2327, 183.6082, 56.6252,
183.2784, 50.5778, 190.9155, 42.7294, 196.8470, 36.1343,
197.7339, 29.9272, 195.5720, 23.4430, 191.2500, 15.9803,
184.0574, 9.5882, 175.8811, 3.9951, 165.7992, 3.4419,
159.0369, 7.4370, 152.5205, 14.8125, 147.4795, 24.2162,
142.4385, 29.0103, 137.0287, 30.9771, 127.1560, 27.4818,
119.1371, 20.0388, 112.1820, 11.3690, 104.6541, 7.1976,
97.2080, 6.2979, 88.9437, 9.8149, 80.3433, 17.3218,
76.5924, 26.5452, 78.1678, 37.0432, 83.5068, 47.1104,
92.8529, 58.3561, 106.3021, 69.2978, 108.9615, 73.9329,
109.0375, 80.6955, 104.4713, 88.6708, 100.6283, 95.7483,
100.1226, 101.5114, 102.8532, 107.2745, 105.6850, 110.9144,
109.7597, 112.9600
]
class BaseShape(Widget):
'''(internal) Base class for moving with touches or calls.'''
# keep references for offset
_old_pos = ListProperty([0, 0])
_old_touch = ListProperty([0, 0])
_new_touch = ListProperty([0, 0])
# shape properties
name = StringProperty('')
poly = ListProperty([])
shape = ObjectProperty()
poly_len = NumericProperty(0)
shape_len = NumericProperty(0)
debug_collider = ObjectProperty()
debug_collider_len = NumericProperty(0)
def __init__(self, **kwargs):
'''Create a shape with size [100, 100]
and give it a label if it's named.
'''
super(BaseShape, self).__init__(**kwargs)
self.size_hint = (None, None)
self.add_widget(Label(text=self.name))
def move_label(self, x, y, *args):
'''Move label with shape name as the only child.'''
self.children[0].pos = [x, y]
def move_collider(self, offset_x, offset_y, *args):
'''Move debug collider when the shape moves.'''
points = self.debug_collider.points[:]
for i in range(0, self.debug_collider_len, 2):
points[i] += offset_x
points[i + 1] += offset_y
self.debug_collider.points = points
def on_debug_collider(self, instance, value):
'''Recalculate length of collider points' array.'''
self.debug_collider_len = len(value.points)
def on_poly(self, instance, value):
'''Recalculate length of polygon points' array.'''
self.poly_len = len(value)
def on_shape(self, instance, value):
'''Recalculate length of Mesh vertices' array.'''
self.shape_len = len(value.vertices)
def on_pos(self, instance, pos):
'''Move polygon and its Mesh on each position change.
This event is above all and changes positions of the other
children-like components, so that a simple::
shape.pos = (100, 200)
would move everything, not just the widget itself.
'''
# position changed by touch
offset_x = self._new_touch[0] - self._old_touch[0]
offset_y = self._new_touch[1] - self._old_touch[1]
# position changed by call (shape.pos = X)
if not offset_x and not offset_y:
offset_x = pos[0] - self._old_pos[0]
offset_y = pos[1] - self._old_pos[1]
self._old_pos = pos
# move polygon points by offset
for i in range(0, self.poly_len, 2):
self.poly[i] += offset_x
self.poly[i + 1] += offset_y
# stick label to bounding box (widget)
if self.name:
self.move_label(*pos)
# move debug collider if available
if self.debug_collider is not None:
self.move_collider(offset_x, offset_y)
# return if no Mesh available
if self.shape is None:
return
# move Mesh vertices by offset
points = self.shape.vertices[:]
for i in range(0, self.shape_len, 2):
points[i] += offset_x
points[i + 1] += offset_y
self.shape.vertices = points
def on_touch_move(self, touch, *args):
'''Move shape with dragging.'''
# grab single touch for shape
if touch.grab_current is not self:
return
# get touches
x, y = touch.pos
new_pos = [x, y]
self._new_touch = new_pos
self._old_touch = [touch.px, touch.py]
# get offsets, move & trigger on_pos event
offset_x = self._new_touch[0] - self._old_touch[0]
offset_y = self._new_touch[1] - self._old_touch[1]
self.pos = [self.x + offset_x, self.y + offset_y]
def shape_collide(self, x, y, *args):
'''Point to polygon collision through a list of points.'''
# ignore if no polygon area is set
poly = self.poly
if not poly:
return False
n = self.poly_len
inside = False
p1x = poly[0]
p1y = poly[1]
# compare point pairs via PIP algo, too long, read
# https://en.wikipedia.org/wiki/Point_in_polygon
for i in range(0, n + 2, 2):
p2x = poly[i % n]
p2y = poly[(i + 1) % n]
if y > min(p1y, p2y) and y <= max(p1y, p2y) and x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
class RegularShape(BaseShape):
'''Starting from center and creating edges around for i.e.:
regular triangles, squares, regular pentagons, up to "circle".
'''
def __init__(self, edges=3, color=None, **kwargs):
super(RegularShape, self).__init__(**kwargs)
if edges < 3:
raise Exception('Not enough edges! (3+ only)')
color = color or [random() for i in range(3)]
rad_edge = (pi * 2) / float(edges)
r_x = self.width / 2.0
r_y = self.height / 2.0
poly = []
vertices = []
for i in range(edges):
# get points within a circle with radius of [r_x, r_y]
x = cos(rad_edge * i) * r_x + self.center_x
y = sin(rad_edge * i) * r_y + self.center_y
poly.extend([x, y])
# add UV layout zeros for Mesh, see Mesh docs
vertices.extend([x, y, 0, 0])
# draw Mesh shape from generated poly points
with self.canvas:
Color(rgba=(color[0], color[1], color[2], 0.6))
self.shape = Mesh(
pos=self.pos,
vertices=vertices,
indices=list(range(edges)),
mode='triangle_fan'
)
self.poly = poly
def on_touch_down(self, touch, *args):
if self.shape_collide(*touch.pos):
touch.grab(self)
class MeshShape(BaseShape):
'''Starting from a custom origin and custom points, draw
a convex Mesh shape with both touch and shape collisions.
.. note::
To get the points, use e.g. Pen tool from your favorite
graphics editor and export it to a human readable format.
'''
def __init__(self, color=None, **kwargs):
super(MeshShape, self).__init__(**kwargs)
color = color or [random() for i in range(3)]
min_x = 10000
min_y = 10000
max_x = 0
max_y = 0
# first point has to be the center of the convex shape's mass,
# that's where the triangle fan starts from
poly = [
50, 50, 0, 0, 100, 0, 100, 100, 0, 100
] if not self.poly else self.poly
# make the polygon smaller to fit 100x100 bounding box
poly = [round(p / 1.5, 4) for p in poly]
poly_len = len(poly)
# create list of vertices & get edges of the polygon
vertices = []
vertices_len = 0
for i in range(0, poly_len, 2):
min_x = poly[i] if poly[i] < min_x else min_x
min_y = poly[i + 1] if poly[i + 1] < min_y else min_y
max_x = poly[i] if poly[i] > max_x else max_x
max_y = poly[i + 1] if poly[i + 1] > max_y else max_y
# add UV layout zeros for Mesh
vertices_len += 4
vertices.extend([poly[i], poly[i + 1], 0, 0])
# get center of poly from edges
poly_center_x, poly_center_y = [
(max_x - min_x) / 2.0,
(max_y - min_y) / 2.0
]
# get distance from the widget's center and push the points to
# the widget's origin, so that min_x and min_y for the poly would
# result in 0 i.e.: points moved as close as possible to [0, 0]
# -> No editor gives poly points moved to the origin directly
dec_x = (self.center_x - poly_center_x) - min_x
dec_y = (self.center_y - poly_center_y) - min_y
# move polygon points to the bounding box (touch)
for i in range(0, poly_len, 2):
poly[i] += dec_x
poly[i + 1] += dec_y
# move mesh points to the bounding box (image)
# has to contain the same points as polygon
for i in range(0, vertices_len, 4):
vertices[i] += dec_x
vertices[i + 1] += dec_y
# draw Mesh shape from generated poly points
with self.canvas:
Color(rgba=(color[0], color[1], color[2], 0.6))
self.shape = Mesh(
pos=self.pos,
vertices=vertices,
indices=list(range(int(poly_len / 2.0))),
mode='triangle_fan'
)
# debug polygon points with Line to see the origin point
# and intersections with the other points
# Line(points=poly)
self.poly = poly
def on_touch_down(self, touch, *args):
if self.shape_collide(*touch.pos):
touch.grab(self)
class Collisions(App):
def __init__(self, **kwargs):
super(Collisions, self).__init__(**kwargs)
# register an event for collision
self.register_event_type('on_collision')
def collision_circles(self, shapes=None, distance=100, debug=False, *args):
'''Simple circle <-> circle collision between the shapes i.e. there's
a simple line between the centers of the two shapes and the collision
is only about measuring distance -> 1+ radii intersections.
'''
# get all combinations from all available shapes
if not hasattr(self, 'combins'):
self.combins = list(combinations(shapes, 2))
for com in self.combins:
x = (com[0].center_x - com[1].center_x) ** 2
y = (com[0].center_y - com[1].center_y) ** 2
if sqrt(x + y) <= distance:
# dispatch a custom event if the objects collide
self.dispatch('on_collision', (com[0], com[1]))
# draw collider only if debugging
if not debug:
return
# add circle collider only if the shape doesn't have one
for shape in shapes:
if shape.debug_collider is not None:
continue
d = distance / 2.0
cx, cy = shape.center
points = [(cx + d * cos(i), cy + d * sin(i)) for i in range(44)]
points = [p for ps in points for p in ps]
with shape.canvas:
Color(rgba=(0, 1, 0, 1))
shape.debug_collider = Point(points=points)
def on_collision(self, pair, *args):
'''Dispatched when objects collide, gives back colliding objects
as a "pair" argument holding their instances.
'''
print('Collision {} x {}'.format(pair[0].name, pair[1].name))
def build(self):
# the environment for all 2D shapes
scene = FloatLayout()
# list of 2D shapes, starting with regular ones
shapes = [
RegularShape(
name='Shape {}'.format(x), edges=x
) for x in range(3, 13)
]
shapes.append(MeshShape(name='DefaultMesh'))
shapes.append(MeshShape(name='Cloud', poly=cloud_poly))
shapes.append(MeshShape(
name='3QuarterCloud',
poly=cloud_poly[:110]
))
# move shapes to some random position
for shape in shapes:
shape.pos = [randint(50, i - 50) for i in Window.size]
scene.add_widget(shape)
# check for simple collisions between the shapes
Clock.schedule_interval(
lambda *t: self.collision_circles(shapes, debug=True), 0.1)
return scene
if __name__ == '__main__':
Collisions().run()
|