def _signed_distance(p1,p2,angle): distance = round(VMath.distance(p1,p2), constant.PRECISION) l_angle = math.atan2(p2[1]-p1[1],p2[0]-p1[0]) diff_angle = VMath.angle_diff(angle,l_angle) if diff_angle > math.pi/2: distance *= -1 return distance
def SAT(o1, o2, moving=None): # Get Edges of the shape hitbox_o1 = o1.hitbox # Store hitbox so no need to recompute them hitbox_o2 = o2.hitbox edges_o1 = _get_edges(hitbox_o1) edges_o2 = _get_edges(hitbox_o2) # Get the axis of the squares and normalize axises = edges_o1[0:2] + edges_o2[0:2] axises = [VMath.normalize(x) for x in axises] # Find the min and max projections for axis in axises: mn = [math.inf]*2 mx = [-math.inf]*2 for p1,p2 in zip(hitbox_o1,hitbox_o2): proj1 = VMath.dot_product(axis, p1) proj2 = VMath.dot_product(axis, p2) mn[0] = min(mn[0], proj1) mx[0] = max(mx[0], proj1) mn[1] = min(mn[1], proj2) mx[1] = max(mx[1], proj2) if mx[1] <= mn[0] or mn[1] >= mx[0]: return 0 return -1
def target(self, enemy_tank, grid): gz = constant.GRID_SIZE tank = grid.get_object(self._object_id) x,y = enemy_tank.position enemy_hitbox = enemy_tank.hitbox gx,gy = int(x/gz), int(y/gz) t_angle = tank.angle mna, mxa = math.inf,-math.inf a1, a2 = None, None for bullet_angle in self._2damap[gx][gy]: trajectory = int(bullet_angle/10) bounce = bullet_angle % 10 if not self._asafe[trajectory]: continue l0 = self._amap[trajectory][bounce-1] if bounce !=0 else tank.position l1 = self._amap[trajectory][bounce] line = [l0,l1] if (collision.line_square(line, enemy_hitbox)): bullet_dir = math.atan2(l1[1]-l0[1],l1[0]-l0[0]) a_diff = VMath.angle_diff(bullet_dir, enemy_tank.angle) a_diff = VMath.astc(a_diff) if a_diff < mna: a1 = trajectory mna = a_diff if a_diff > mxa: a2 = trajectory mxa = a_diff if a1 is None and a2 is None: return False if a1 == a2: return [a1] else: return [a1,a2]
def get_distance(o1, o2, moving=constant.FORWARD): min_dist = math.inf moving -= 1 # Forward: 0, Reverse: 1 c_line = None # X and Y Distances x,y = VMath.subtract(o1.position, o2.position) # Various angles a1 = o1.angle a2 = o2.angle a1_r = (a1 + math.pi) % math.tau # Obj Velocity Direction (reverse) if moving: a1, a1_r = a1_r, a1 # Calculates and stores hitbox of objects for future use, saves recomputing. hitbox_obj1 = o1.hitbox # So that code does not need to hitbox_obj2 = o2.hitbox # recalculate hitboxes, saving computing time. # Get points & lines of both objects that have the potential of colliding. # 1 | 0 # --------- # 2 | 3 # q = int(2*(a1-a2+3*math.pi)/math.pi%4) # Quadrant of points that will collide w/ o2 h2 = hitbox_obj2[q:q+3] + hitbox_obj2[:max(q-1,0)] lines_o1 = [hitbox_obj1[1:3], (hitbox_obj1[0],hitbox_obj1[3])] lines_o2 = [h2[0:2], h2[1:3]] # Rays from o1 -> o2 for i in lines_o1: for j in lines_o2: p = line_intersection(i, j) if p and _point_in_line(p,j): distance = _signed_distance(i[moving],p,a1) if distance >= 0 and distance < min_dist: min_dist = distance c_line = j if not c_line: return math.inf, None # Rays from o2 -> o1 l_o1 = hitbox_obj1[0+2*moving:2+2*moving] l_o2 = [h2[1], VMath.translate(h2[1], 1, a1)] p = line_intersection(l_o2,l_o1) if p and _point_in_line(p,l_o1): distance = _signed_distance(p,h2[1],a1) if distance >= 0 and distance < min_dist: min_dist = distance if VMath.distance(p, l_o1[0]) < VMath.distance(p, l_o1[1]): c_line = lines_o2[moving] else: c_line = lines_o2[1-moving] if min_dist is not math.inf: min_dist = min_dist return min_dist, c_line
def render_entities(self, tick, time_step, stage): surface = self._bg.copy().convert_alpha() controllers = self._grid.get_controllers() show_name = (self._keybind.get_keys() & constant.SHOW_NAMES) | stage for i in range(len(controllers)): controller = controllers[i] obj = self._grid.get_object(controller.object_id) if isinstance(obj, tank.Tank): if not isinstance(controller, playerController.PlayerController): stage = False player = self._itop[controller.object_id] self._renderer.render_tank(surface, obj, player, time_step, show_name=show_name, show_arrow=stage) else: self._renderer.render_bullet(surface, obj, time_step, colour=None) gz = constant.GRID_SIZE for ff in temp: path = pygame.Surface((gz,gz)) path.set_alpha(128) path.fill(ff[2]) surface.blit(path,ff[0:2]) for lin in line: pygame.draw.line(surface, (0,0,0), lin[0], VMath.translate(lin[0],500,lin[1])) return surface
def shoot(self, grid): tank = grid.get_object(self._object_id) x,y = VMath.translate(tank.position, tank.nozzle_length, tank.nozzle_angle) projectile = bullet.Bullet(x,y,tank.nozzle_angle) id = grid.add_object(projectile) if id: grid.add_controller(bulletController.BulletController(id, self)) else: return False return True
def _bounce_object(self, obj, dist, line): id = self._ids[obj] if dist: obj.move(dist) x, y = VMath.subtract(line[0], line[1]) l_angle = math.atan2(y, x) % math.pi #print(f"before collision ({obj.angle*180/math.pi}): {obj.hitbox}") obj.force_rotate(2 * l_angle - obj.angle)
def _line_at_angle(line, angle): p1,p2=line x,y = VMath.subtract(p2,p1) l_angle = math.atan2(y,x) if l_angle < 0: l_angle += math.tau angle_diff = abs(angle - l_angle) angle_diff = min(angle_diff, math.tau-angle_diff) if abs(angle_diff) <= math.pi/2: return True return False
def render_entity(self, surface, sprite, entity, time_step, angle=None, pivot=[0, 0]): if isinstance(sprite, pygame.Surface): tile = sprite else: i, j = sprite tile = self._sprite_sheet[i][j].copy().convert_alpha() if not angle: a1, a2 = entity.old_angle, entity.angle else: a1, a2 = angle[0], angle[1] angle = a1 + VMath.angle_diff(a1, a2) * time_step rot_tile = pygame.transform.rotate(tile, -math.degrees(angle)) center = rot_tile.get_width() / 2, rot_tile.get_height() / 2 x1, y1 = entity.position x0, y0 = entity.old_position entity_pos = x0 + (x1 - x0) * time_step, y0 + (y1 - y0) * time_step pos = VMath.subtract(entity_pos, center) # Account for pivot pos = VMath.subtract(pos, VMath.rotate([pivot], angle)[0]) surface.blit(rot_tile, pos)
def beamV3(self, grid, pos=None, angle=None, hitbox=None, bounce=constant.BULLET_BOUNCE, old_angle=None): tank = grid.get_object(self._object_id) if pos is None: pos = tank.position if angle is None: angle = tank.nozzle_angle if hitbox is None: hitbox = tank.hitbox if old_angle is None: old_angle = angle new_position = None new_angle = None cpb = [0]*2 # collision point boundary (1: x, 2: y) hc = [0]*2 # left or right point of bullet wall, has it collided? ctr = -1 # counter, once a collision with one of the ray hits, # the 2nd ray can move only up to two steps. bpt = [] # Return Variables # bp = [] # store the grid index of the bullet path bep = [] # stores the bullet end points ct = False # Determines whether trajectory has collided with tank gz = constant.GRID_SIZE (bw,bh) = constant.BULLET_SIZE # Bullet width/height # Get the starting position of the two rays which define the trajectory # the bullet will take and find the grid position of those two points. offset = VMath.rotate([(0.5*bw,0.5*bh),(0.5*bw,-0.5*bh)], angle) pts = VMath.sum(pos,offset[0]), VMath.sum(pos,offset[1]) gpts = [int(pts[0][0]/gz),int(pts[0][1]/gz)],[int(pts[1][0]/gz),int(pts[1][1]/gz)] ### DEBUGGER ### ''' if bounce == constant.BULLET_BOUNCE: game.temp = [] game.line = [] game.line.append([pts[0], angle]) game.line.append([pts[1], angle]) #print(f"b: {bounce}, l1: y-{pts[0][1]} = {math.tan(angle)}(x-{pts[0][0]})") #print(f"b: {bounce}, l2: y-{pts[1][1]} = {math.tan(angle)}(x-{pts[1][0]})") ''' # Find velocity vector of the bullet h_angle = math.pi/2 - angle vx,vy = sin(h_angle),cos(h_angle) sx,sy = int(math.copysign(1,vx)), int(math.copysign(1,vy)) # Given a velocity vector V and intial position U, find dx, dy # such that U+V*dx and U+V*dy will be a point that intersect # the x and y grid edges respectively. Note, mxdx, mxdy is the # distance the projectile needs to travel one horizontal/vertical grid tile. dx = [math.inf]*2 dy = [math.inf]*2 if round(vx,5): mxdx = gz/abs(vx) dx[0] = mxdx-(pts[0][0]*sx%gz)*sx/vx dx[1] = mxdx-(pts[1][0]*sx%gz)*sx/vx if round(vy,5): mxdy = gz/abs(vy) dy[0] = mxdy-(pts[0][1]*sy%gz)*sy/vy dy[1] = mxdy-(pts[1][1]*sy%gz)*sy/vy mx = 1 if dx[1]<dx[0] else 0 # Find min_x my = 1 if dy[1]<dy[0] else 0 # Find min_y # March dx/dy as specified in paper and stop marching when a collision # occurs. Keep track of what collision happened first and whether it # was a horizontal or vertical collision. while not (cpb[0] and cpb[1]) and ctr: if dx[mx] < dy[my]: gpts[mx][0] += sx if self.check_solid(grid[gpts[mx][0],gpts[mx][1]]): cpb[mx] = 'x' ctr = 3 hc[mx] = hc[1-mx]+1 if hc[my]: my = 1 - my else: dx[mx] += mxdx bgpt = gpts[mx][0],gpts[mx][1] if not bpt or bpt[-1] != bgpt: bpt.append(bgpt) if not hc[1-mx]: mx = 1 - mx else: gpts[my][1] += sy if self.check_solid(grid[gpts[my][0],gpts[my][1]]): cpb[my] = 'y' ctr = 3 hc[my] = hc[1-my]+1 if hc[mx]: mx = 1 - mx else: dy[my] += mxdy bgpt = gpts[mx][0],gpts[mx][1] if not bpt or bpt[-1] != bgpt: bpt.append(bgpt) if not hc[1-my]: my = 1 - my ctr -= 1 # From the three scenarios mentioned above # Find collision by X or Y axis # Find collsiion is at p1 or p2 angles = [(math.pi-angle)%math.tau, (-angle)%math.tau] cpt = 0 # Get position/angle based on scenario if VMath.equals(gpts[mx],gpts[my]) and cpb[0] and cpb[1] and cpb[0] != cpb[1]: grid_corner = (gpts[0][0]*gz+0.5*gz*(1-sx),gpts[0][1]*gz+0.5*gz*(1-sy)) bp0 = collision.line_intersection2(pts[0],angle,grid_corner,angle-math.pi/2) bpd = VMath.distance(bp0, grid_corner) if VMath.distance(bp0, grid_corner) > 0.5*bh: ca = 1 if sx*sy>0 else 0 else: ca = 0 if sx*sy>0 else 1 new_pos = VMath.subtract(bp0, offset[0]) new_angle = angles[1-ca] else: if hc[0] == 1: ca = ord(cpb[0]) - ord('x') else: ca = ord(cpb[1]) - ord('x') if hc[1] == 1: cpt = 1 d = dy[cpt] if ca else dx[cpt] pt = (pts[cpt][0]+vx*d,pts[cpt][1]+vy*d) new_pos = VMath.subtract(pt, offset[cpt]) new_angle = angles[ca] # Check if this point has passed the tank cpt = None pts2 = VMath.sum(new_pos,offset[0]), VMath.sum(new_pos,offset[1]) if bounce < constant.BULLET_BOUNCE: cpt = collision.line_square((pts[0], pts2[0]), hitbox) if cpt: cpt = VMath.subtract(cpt,offset[0]) else: cpt = collision.line_square((pts[1], pts2[1]), hitbox) if cpt: cpt = VMath.subtract(cpt,offset[1]) if cpt: bep.append(cpt) ct = True gx, gy = int(cpt[0]/gz), int(cpt[1]/gz) else: bep.append(new_pos) if bounce: bpf, bepf, ctf = self.beamV3(grid, new_pos, new_angle, hitbox, bounce-1, old_angle) bep.extend(bepf) bp.extend(bpf) if ctf: ct = True if not cpt: for p in bpt: bp.append((p,bounce)) else: for p in bpt: if p[0]*sx < gx*sx and p[1]*sy < gy*sy: bp.append((p,bounce)) return bp, bep, ct
def _get_radius(self): r = 0 for point in self._hitbox: dist = VMath.distance((0, 0), point) if dist > r: r = dist return r
def move(self, distance): self.position = VMath.translate(self.position, distance, self._angle)
def rotate(self, angle): self._hitbox = VMath.rotate(self._hitbox, angle) self._angle = (self._angle + angle) % math.tau
def _get_edges(points): edges = [] for i in range(4): edges.append(VMath.subtract(points[i],points[(i+1)%4])) return edges
def _point_in_line(point, line): b,(a,c) = point,line d_ab = VMath.distance(a,b) d_bc = VMath.distance(b,c) d_ac = VMath.distance(a,c) return not round(abs(d_ab + d_bc - d_ac), constant.PRECISION)
def circle(o1, o2, extra=0): dist = VMath.distance(o1.position, o2.position) if o1.radius + o2.radius + extra> dist: return True return False