1641 lines
69 KiB
Python
Executable File
1641 lines
69 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Copyright (c) 2019 Computer Vision Center (CVC) at the Universitat Autonoma de
|
|
# Barcelona (UAB).
|
|
#
|
|
# This work is licensed under the terms of the MIT license.
|
|
# For a copy, see <https://opensource.org/licenses/MIT>.
|
|
|
|
# Allows visualising a 2D map generated by vehicles.
|
|
|
|
"""
|
|
Welcome to CARLA No-Rendering Mode Visualizer
|
|
|
|
TAB : toggle hero mode
|
|
Mouse Wheel : zoom in / zoom out
|
|
Mouse Drag : move map (map mode only)
|
|
|
|
W : throttle
|
|
S : brake
|
|
AD : steer
|
|
Q : toggle reverse
|
|
Space : hand-brake
|
|
P : toggle autopilot
|
|
M : toggle manual transmission
|
|
,/. : gear up/down
|
|
|
|
F1 : toggle HUD
|
|
I : toggle actor ids
|
|
H/? : toggle help
|
|
ESC : quit
|
|
"""
|
|
|
|
# ==============================================================================
|
|
# -- find carla module ---------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
import glob
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
sys.path.append(glob.glob('../carla/dist/carla-*%d.%d-%s.egg' % (
|
|
sys.version_info.major,
|
|
sys.version_info.minor,
|
|
'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0])
|
|
except IndexError:
|
|
pass
|
|
|
|
# ==============================================================================
|
|
# -- imports -------------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
import carla
|
|
from carla import TrafficLightState as tls
|
|
|
|
import argparse
|
|
import logging
|
|
import datetime
|
|
import weakref
|
|
import math
|
|
import random
|
|
import hashlib
|
|
|
|
try:
|
|
import pygame
|
|
from pygame.locals import KMOD_CTRL
|
|
from pygame.locals import KMOD_SHIFT
|
|
from pygame.locals import K_COMMA
|
|
from pygame.locals import K_DOWN
|
|
from pygame.locals import K_ESCAPE
|
|
from pygame.locals import K_F1
|
|
from pygame.locals import K_LEFT
|
|
from pygame.locals import K_PERIOD
|
|
from pygame.locals import K_RIGHT
|
|
from pygame.locals import K_SLASH
|
|
from pygame.locals import K_SPACE
|
|
from pygame.locals import K_TAB
|
|
from pygame.locals import K_UP
|
|
from pygame.locals import K_a
|
|
from pygame.locals import K_d
|
|
from pygame.locals import K_h
|
|
from pygame.locals import K_i
|
|
from pygame.locals import K_m
|
|
from pygame.locals import K_p
|
|
from pygame.locals import K_q
|
|
from pygame.locals import K_s
|
|
from pygame.locals import K_w
|
|
except ImportError:
|
|
raise RuntimeError('cannot import pygame, make sure pygame package is installed')
|
|
|
|
# ==============================================================================
|
|
# -- Constants -----------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
# Colors
|
|
|
|
# We will use the color palette used in Tango Desktop Project (Each color is indexed depending on brightness level)
|
|
# See: https://en.wikipedia.org/wiki/Tango_Desktop_Project
|
|
|
|
COLOR_BUTTER_0 = pygame.Color(252, 233, 79)
|
|
COLOR_BUTTER_1 = pygame.Color(237, 212, 0)
|
|
COLOR_BUTTER_2 = pygame.Color(196, 160, 0)
|
|
|
|
COLOR_ORANGE_0 = pygame.Color(252, 175, 62)
|
|
COLOR_ORANGE_1 = pygame.Color(245, 121, 0)
|
|
COLOR_ORANGE_2 = pygame.Color(209, 92, 0)
|
|
|
|
COLOR_CHOCOLATE_0 = pygame.Color(233, 185, 110)
|
|
COLOR_CHOCOLATE_1 = pygame.Color(193, 125, 17)
|
|
COLOR_CHOCOLATE_2 = pygame.Color(143, 89, 2)
|
|
|
|
COLOR_CHAMELEON_0 = pygame.Color(138, 226, 52)
|
|
COLOR_CHAMELEON_1 = pygame.Color(115, 210, 22)
|
|
COLOR_CHAMELEON_2 = pygame.Color(78, 154, 6)
|
|
|
|
COLOR_SKY_BLUE_0 = pygame.Color(114, 159, 207)
|
|
COLOR_SKY_BLUE_1 = pygame.Color(52, 101, 164)
|
|
COLOR_SKY_BLUE_2 = pygame.Color(32, 74, 135)
|
|
|
|
COLOR_PLUM_0 = pygame.Color(173, 127, 168)
|
|
COLOR_PLUM_1 = pygame.Color(117, 80, 123)
|
|
COLOR_PLUM_2 = pygame.Color(92, 53, 102)
|
|
|
|
COLOR_SCARLET_RED_0 = pygame.Color(239, 41, 41)
|
|
COLOR_SCARLET_RED_1 = pygame.Color(204, 0, 0)
|
|
COLOR_SCARLET_RED_2 = pygame.Color(164, 0, 0)
|
|
|
|
COLOR_ALUMINIUM_0 = pygame.Color(238, 238, 236)
|
|
COLOR_ALUMINIUM_1 = pygame.Color(211, 215, 207)
|
|
COLOR_ALUMINIUM_2 = pygame.Color(186, 189, 182)
|
|
COLOR_ALUMINIUM_3 = pygame.Color(136, 138, 133)
|
|
COLOR_ALUMINIUM_4 = pygame.Color(85, 87, 83)
|
|
COLOR_ALUMINIUM_4_5 = pygame.Color(66, 62, 64)
|
|
COLOR_ALUMINIUM_5 = pygame.Color(46, 52, 54)
|
|
|
|
|
|
COLOR_WHITE = pygame.Color(255, 255, 255)
|
|
COLOR_BLACK = pygame.Color(0, 0, 0)
|
|
|
|
# Module Defines
|
|
TITLE_WORLD = 'WORLD'
|
|
TITLE_HUD = 'HUD'
|
|
TITLE_INPUT = 'INPUT'
|
|
|
|
PIXELS_PER_METER = 12
|
|
|
|
MAP_DEFAULT_SCALE = 0.1
|
|
HERO_DEFAULT_SCALE = 1.0
|
|
|
|
PIXELS_AHEAD_VEHICLE = 150
|
|
|
|
# ==============================================================================
|
|
# -- Util -----------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
def get_actor_display_name(actor, truncate=250):
|
|
name = ' '.join(actor.type_id.replace('_', '.').title().split('.')[1:])
|
|
return (name[:truncate - 1] + u'\u2026') if len(name) > truncate else name
|
|
|
|
|
|
class Util(object):
|
|
|
|
@staticmethod
|
|
def blits(destination_surface, source_surfaces, rect=None, blend_mode=0):
|
|
"""Function that renders the all the source surfaces in a destination source"""
|
|
for surface in source_surfaces:
|
|
destination_surface.blit(surface[0], surface[1], rect, blend_mode)
|
|
|
|
@staticmethod
|
|
def length(v):
|
|
"""Returns the length of a vector"""
|
|
return math.sqrt(v.x**2 + v.y**2 + v.z**2)
|
|
|
|
@staticmethod
|
|
def get_bounding_box(actor):
|
|
"""Gets the bounding box corners of an actor in world space"""
|
|
bb = actor.trigger_volume.extent
|
|
corners = [carla.Location(x=-bb.x, y=-bb.y),
|
|
carla.Location(x=bb.x, y=-bb.y),
|
|
carla.Location(x=bb.x, y=bb.y),
|
|
carla.Location(x=-bb.x, y=bb.y),
|
|
carla.Location(x=-bb.x, y=-bb.y)]
|
|
corners = [x + actor.trigger_volume.location for x in corners]
|
|
t = actor.get_transform()
|
|
t.transform(corners)
|
|
return corners
|
|
|
|
# ==============================================================================
|
|
# -- FadingText ----------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
class FadingText(object):
|
|
"""Renders texts that fades out after some seconds that the user specifies"""
|
|
|
|
def __init__(self, font, dim, pos):
|
|
"""Initializes variables such as text font, dimensions and position"""
|
|
self.font = font
|
|
self.dim = dim
|
|
self.pos = pos
|
|
self.seconds_left = 0
|
|
self.surface = pygame.Surface(self.dim)
|
|
|
|
def set_text(self, text, color=COLOR_WHITE, seconds=2.0):
|
|
"""Sets the text, color and seconds until fade out"""
|
|
text_texture = self.font.render(text, True, color)
|
|
self.surface = pygame.Surface(self.dim)
|
|
self.seconds_left = seconds
|
|
self.surface.fill(COLOR_BLACK)
|
|
self.surface.blit(text_texture, (10, 11))
|
|
|
|
def tick(self, clock):
|
|
"""Each frame, it shows the displayed text for some specified seconds, if any"""
|
|
delta_seconds = 1e-3 * clock.get_time()
|
|
self.seconds_left = max(0.0, self.seconds_left - delta_seconds)
|
|
self.surface.set_alpha(500.0 * self.seconds_left)
|
|
|
|
def render(self, display):
|
|
""" Renders the text in its surface and its position"""
|
|
display.blit(self.surface, self.pos)
|
|
|
|
|
|
# ==============================================================================
|
|
# -- HelpText ------------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
class HelpText(object):
|
|
def __init__(self, font, width, height):
|
|
"""Renders the help text that shows the controls for using no rendering mode"""
|
|
lines = __doc__.split('\n')
|
|
self.font = font
|
|
self.dim = (680, len(lines) * 22 + 12)
|
|
self.pos = (0.5 * width - 0.5 * self.dim[0], 0.5 * height - 0.5 * self.dim[1])
|
|
self.seconds_left = 0
|
|
self.surface = pygame.Surface(self.dim)
|
|
self.surface.fill(COLOR_BLACK)
|
|
for n, line in enumerate(lines):
|
|
text_texture = self.font.render(line, True, COLOR_WHITE)
|
|
self.surface.blit(text_texture, (22, n * 22))
|
|
self._render = False
|
|
self.surface.set_alpha(220)
|
|
|
|
def toggle(self):
|
|
"""Toggles display of help text"""
|
|
self._render = not self._render
|
|
|
|
def render(self, display):
|
|
"""Renders the help text, if enabled"""
|
|
if self._render:
|
|
display.blit(self.surface, self.pos)
|
|
|
|
|
|
# ==============================================================================
|
|
# -- HUD -----------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
class HUD (object):
|
|
"""Class encharged of rendering the HUD that displays information about the world and the hero vehicle"""
|
|
|
|
def __init__(self, name, width, height):
|
|
"""Initializes default HUD params and content data parameters that will be displayed"""
|
|
self.name = name
|
|
self.dim = (width, height)
|
|
self._init_hud_params()
|
|
self._init_data_params()
|
|
|
|
def start(self):
|
|
"""Does nothing since it does not need to use other modules"""
|
|
|
|
def _init_hud_params(self):
|
|
"""Initialized visual parameters such as font text and size"""
|
|
font_name = 'courier' if os.name == 'nt' else 'mono'
|
|
fonts = [x for x in pygame.font.get_fonts() if font_name in x]
|
|
default_font = 'ubuntumono'
|
|
mono = default_font if default_font in fonts else fonts[0]
|
|
mono = pygame.font.match_font(mono)
|
|
self._font_mono = pygame.font.Font(mono, 14)
|
|
self._header_font = pygame.font.SysFont('Arial', 14, True)
|
|
self.help = HelpText(pygame.font.Font(mono, 24), *self.dim)
|
|
self._notifications = FadingText(
|
|
pygame.font.Font(pygame.font.get_default_font(), 20),
|
|
(self.dim[0], 40), (0, self.dim[1] - 40))
|
|
|
|
def _init_data_params(self):
|
|
"""Initializes the content data structures"""
|
|
self.show_info = True
|
|
self.show_actor_ids = False
|
|
self._info_text = {}
|
|
|
|
def notification(self, text, seconds=2.0):
|
|
"""Shows fading texts for some specified seconds"""
|
|
self._notifications.set_text(text, seconds=seconds)
|
|
|
|
def tick(self, clock):
|
|
"""Updated the fading texts each frame"""
|
|
self._notifications.tick(clock)
|
|
|
|
def add_info(self, title, info):
|
|
"""Adds a block of information in the left HUD panel of the visualizer"""
|
|
self._info_text[title] = info
|
|
|
|
def render_vehicles_ids(self, vehicle_id_surface, list_actors, world_to_pixel, hero_actor, hero_transform):
|
|
"""When flag enabled, it shows the IDs of the vehicles that are spawned in the world. Depending on the vehicle type,
|
|
it will render it in different colors"""
|
|
|
|
vehicle_id_surface.fill(COLOR_BLACK)
|
|
if self.show_actor_ids:
|
|
vehicle_id_surface.set_alpha(150)
|
|
for actor in list_actors:
|
|
x, y = world_to_pixel(actor[1].location)
|
|
|
|
angle = 0
|
|
if hero_actor is not None:
|
|
angle = -hero_transform.rotation.yaw - 90
|
|
|
|
color = COLOR_SKY_BLUE_0
|
|
if int(actor[0].attributes['number_of_wheels']) == 2:
|
|
color = COLOR_CHOCOLATE_0
|
|
if actor[0].attributes['role_name'] == 'hero':
|
|
color = COLOR_CHAMELEON_0
|
|
|
|
font_surface = self._header_font.render(str(actor[0].id), True, color)
|
|
rotated_font_surface = pygame.transform.rotate(font_surface, angle)
|
|
rect = rotated_font_surface.get_rect(center=(x, y))
|
|
vehicle_id_surface.blit(rotated_font_surface, rect)
|
|
|
|
return vehicle_id_surface
|
|
|
|
def render(self, display):
|
|
"""If flag enabled, it renders all the information regarding the left panel of the visualizer"""
|
|
if self.show_info:
|
|
info_surface = pygame.Surface((240, self.dim[1]))
|
|
info_surface.set_alpha(100)
|
|
display.blit(info_surface, (0, 0))
|
|
v_offset = 4
|
|
bar_h_offset = 100
|
|
bar_width = 106
|
|
i = 0
|
|
for title, info in self._info_text.items():
|
|
if not info:
|
|
continue
|
|
surface = self._header_font.render(title, True, COLOR_ALUMINIUM_0).convert_alpha()
|
|
display.blit(surface, (8 + bar_width / 2, 18 * i + v_offset))
|
|
v_offset += 12
|
|
i += 1
|
|
for item in info:
|
|
if v_offset + 18 > self.dim[1]:
|
|
break
|
|
if isinstance(item, list):
|
|
if len(item) > 1:
|
|
points = [(x + 8, v_offset + 8 + (1.0 - y) * 30) for x, y in enumerate(item)]
|
|
pygame.draw.lines(display, (255, 136, 0), False, points, 2)
|
|
item = None
|
|
elif isinstance(item, tuple):
|
|
if isinstance(item[1], bool):
|
|
rect = pygame.Rect((bar_h_offset, v_offset + 8), (6, 6))
|
|
pygame.draw.rect(display, COLOR_ALUMINIUM_0, rect, 0 if item[1] else 1)
|
|
else:
|
|
rect_border = pygame.Rect((bar_h_offset, v_offset + 8), (bar_width, 6))
|
|
pygame.draw.rect(display, COLOR_ALUMINIUM_0, rect_border, 1)
|
|
f = (item[1] - item[2]) / (item[3] - item[2])
|
|
if item[2] < 0.0:
|
|
rect = pygame.Rect((bar_h_offset + f * (bar_width - 6), v_offset + 8), (6, 6))
|
|
else:
|
|
rect = pygame.Rect((bar_h_offset, v_offset + 8), (f * bar_width, 6))
|
|
pygame.draw.rect(display, COLOR_ALUMINIUM_0, rect)
|
|
item = item[0]
|
|
if item: # At this point has to be a str.
|
|
surface = self._font_mono.render(item, True, COLOR_ALUMINIUM_0).convert_alpha()
|
|
display.blit(surface, (8, 18 * i + v_offset))
|
|
v_offset += 18
|
|
v_offset += 24
|
|
self._notifications.render(display)
|
|
self.help.render(display)
|
|
|
|
|
|
# ==============================================================================
|
|
# -- TrafficLightSurfaces ------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
class TrafficLightSurfaces(object):
|
|
"""Holds the surfaces (scaled and rotated) for painting traffic lights"""
|
|
|
|
def __init__(self):
|
|
def make_surface(tl):
|
|
"""Draws a traffic light, which is composed of a dark background surface with 3 circles that indicate its color depending on the state"""
|
|
w = 40
|
|
surface = pygame.Surface((w, 3 * w), pygame.SRCALPHA)
|
|
surface.fill(COLOR_ALUMINIUM_5 if tl != 'h' else COLOR_ORANGE_2)
|
|
if tl != 'h':
|
|
hw = int(w / 2)
|
|
off = COLOR_ALUMINIUM_4
|
|
red = COLOR_SCARLET_RED_0
|
|
yellow = COLOR_BUTTER_0
|
|
green = COLOR_CHAMELEON_0
|
|
|
|
# Draws the corresponding color if is on, otherwise it will be gray if its off
|
|
pygame.draw.circle(surface, red if tl == tls.Red else off, (hw, hw), int(0.4 * w))
|
|
pygame.draw.circle(surface, yellow if tl == tls.Yellow else off, (hw, w + hw), int(0.4 * w))
|
|
pygame.draw.circle(surface, green if tl == tls.Green else off, (hw, 2 * w + hw), int(0.4 * w))
|
|
|
|
return pygame.transform.smoothscale(surface, (15, 45) if tl != 'h' else (19, 49))
|
|
|
|
self._original_surfaces = {
|
|
'h': make_surface('h'),
|
|
tls.Red: make_surface(tls.Red),
|
|
tls.Yellow: make_surface(tls.Yellow),
|
|
tls.Green: make_surface(tls.Green),
|
|
tls.Off: make_surface(tls.Off),
|
|
tls.Unknown: make_surface(tls.Unknown)
|
|
}
|
|
self.surfaces = dict(self._original_surfaces)
|
|
|
|
def rotozoom(self, angle, scale):
|
|
"""Rotates and scales the traffic light surface"""
|
|
for key, surface in self._original_surfaces.items():
|
|
self.surfaces[key] = pygame.transform.rotozoom(surface, angle, scale)
|
|
|
|
|
|
# ==============================================================================
|
|
# -- World ---------------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
class MapImage(object):
|
|
"""Class encharged of rendering a 2D image from top view of a carla world. Please note that a cache system is used, so if the OpenDrive content
|
|
of a Carla town has not changed, it will read and use the stored image if it was rendered in a previous execution"""
|
|
|
|
def __init__(self, carla_world, carla_map, pixels_per_meter, show_triggers, show_connections, show_spawn_points):
|
|
""" Renders the map image generated based on the world, its map and additional flags that provide extra information about the road network"""
|
|
self._pixels_per_meter = pixels_per_meter
|
|
self.scale = 1.0
|
|
self.show_triggers = show_triggers
|
|
self.show_connections = show_connections
|
|
self.show_spawn_points = show_spawn_points
|
|
|
|
waypoints = carla_map.generate_waypoints(2)
|
|
margin = 50
|
|
max_x = max(waypoints, key=lambda x: x.transform.location.x).transform.location.x + margin
|
|
max_y = max(waypoints, key=lambda x: x.transform.location.y).transform.location.y + margin
|
|
min_x = min(waypoints, key=lambda x: x.transform.location.x).transform.location.x - margin
|
|
min_y = min(waypoints, key=lambda x: x.transform.location.y).transform.location.y - margin
|
|
|
|
self.width = max(max_x - min_x, max_y - min_y)
|
|
self._world_offset = (min_x, min_y)
|
|
|
|
# Maximum size of a Pygame surface
|
|
width_in_pixels = (1 << 14) - 1
|
|
|
|
# Adapt Pixels per meter to make world fit in surface
|
|
surface_pixel_per_meter = int(width_in_pixels / self.width)
|
|
if surface_pixel_per_meter > PIXELS_PER_METER:
|
|
surface_pixel_per_meter = PIXELS_PER_METER
|
|
|
|
self._pixels_per_meter = surface_pixel_per_meter
|
|
width_in_pixels = int(self._pixels_per_meter * self.width)
|
|
|
|
self.big_map_surface = pygame.Surface((width_in_pixels, width_in_pixels)).convert()
|
|
|
|
# Load OpenDrive content
|
|
opendrive_content = carla_map.to_opendrive()
|
|
|
|
# Get hash based on content
|
|
hash_func = hashlib.sha1()
|
|
hash_func.update(opendrive_content.encode("UTF-8"))
|
|
opendrive_hash = str(hash_func.hexdigest())
|
|
|
|
# Build path for saving or loading the cached rendered map
|
|
filename = carla_map.name + "_" + opendrive_hash + ".tga"
|
|
dirname = os.path.join("cache", "no_rendering_mode")
|
|
full_path = str(os.path.join(dirname, filename))
|
|
|
|
if os.path.isfile(full_path):
|
|
# Load Image
|
|
self.big_map_surface = pygame.image.load(full_path)
|
|
else:
|
|
# Render map
|
|
self.draw_road_map(
|
|
self.big_map_surface,
|
|
carla_world,
|
|
carla_map,
|
|
self.world_to_pixel,
|
|
self.world_to_pixel_width)
|
|
|
|
# If folders path does not exist, create it
|
|
if not os.path.exists(dirname):
|
|
os.makedirs(dirname)
|
|
|
|
# Remove files if selected town had a previous version saved
|
|
list_filenames = glob.glob(os.path.join(dirname, carla_map.name) + "*")
|
|
for town_filename in list_filenames:
|
|
os.remove(town_filename)
|
|
|
|
# Save rendered map for next executions of same map
|
|
pygame.image.save(self.big_map_surface, full_path)
|
|
|
|
self.surface = self.big_map_surface
|
|
|
|
def draw_road_map(self, map_surface, carla_world, carla_map, world_to_pixel, world_to_pixel_width):
|
|
"""Draws all the roads, including lane markings, arrows and traffic signs"""
|
|
map_surface.fill(COLOR_ALUMINIUM_4)
|
|
precision = 0.05
|
|
|
|
def lane_marking_color_to_tango(lane_marking_color):
|
|
"""Maps the lane marking color enum specified in PythonAPI to a Tango Color"""
|
|
tango_color = COLOR_BLACK
|
|
|
|
if lane_marking_color == carla.LaneMarkingColor.White:
|
|
tango_color = COLOR_ALUMINIUM_2
|
|
|
|
elif lane_marking_color == carla.LaneMarkingColor.Blue:
|
|
tango_color = COLOR_SKY_BLUE_0
|
|
|
|
elif lane_marking_color == carla.LaneMarkingColor.Green:
|
|
tango_color = COLOR_CHAMELEON_0
|
|
|
|
elif lane_marking_color == carla.LaneMarkingColor.Red:
|
|
tango_color = COLOR_SCARLET_RED_0
|
|
|
|
elif lane_marking_color == carla.LaneMarkingColor.Yellow:
|
|
tango_color = COLOR_ORANGE_0
|
|
|
|
return tango_color
|
|
|
|
def draw_solid_line(surface, color, closed, points, width):
|
|
"""Draws solid lines in a surface given a set of points, width and color"""
|
|
if len(points) >= 2:
|
|
pygame.draw.lines(surface, color, closed, points, width)
|
|
|
|
def draw_broken_line(surface, color, closed, points, width):
|
|
"""Draws broken lines in a surface given a set of points, width and color"""
|
|
# Select which lines are going to be rendered from the set of lines
|
|
broken_lines = [x for n, x in enumerate(zip(*(iter(points),) * 20)) if n % 3 == 0]
|
|
|
|
# Draw selected lines
|
|
for line in broken_lines:
|
|
pygame.draw.lines(surface, color, closed, line, width)
|
|
|
|
def get_lane_markings(lane_marking_type, lane_marking_color, waypoints, sign):
|
|
"""For multiple lane marking types (SolidSolid, BrokenSolid, SolidBroken and BrokenBroken), it converts them
|
|
as a combination of Broken and Solid lines"""
|
|
margin = 0.25
|
|
marking_1 = [world_to_pixel(lateral_shift(w.transform, sign * w.lane_width * 0.5)) for w in waypoints]
|
|
if lane_marking_type == carla.LaneMarkingType.Broken or (lane_marking_type == carla.LaneMarkingType.Solid):
|
|
return [(lane_marking_type, lane_marking_color, marking_1)]
|
|
else:
|
|
marking_2 = [world_to_pixel(lateral_shift(w.transform,
|
|
sign * (w.lane_width * 0.5 + margin * 2))) for w in waypoints]
|
|
if lane_marking_type == carla.LaneMarkingType.SolidBroken:
|
|
return [(carla.LaneMarkingType.Broken, lane_marking_color, marking_1),
|
|
(carla.LaneMarkingType.Solid, lane_marking_color, marking_2)]
|
|
elif lane_marking_type == carla.LaneMarkingType.BrokenSolid:
|
|
return [(carla.LaneMarkingType.Solid, lane_marking_color, marking_1),
|
|
(carla.LaneMarkingType.Broken, lane_marking_color, marking_2)]
|
|
elif lane_marking_type == carla.LaneMarkingType.BrokenBroken:
|
|
return [(carla.LaneMarkingType.Broken, lane_marking_color, marking_1),
|
|
(carla.LaneMarkingType.Broken, lane_marking_color, marking_2)]
|
|
elif lane_marking_type == carla.LaneMarkingType.SolidSolid:
|
|
return [(carla.LaneMarkingType.Solid, lane_marking_color, marking_1),
|
|
(carla.LaneMarkingType.Solid, lane_marking_color, marking_2)]
|
|
|
|
return [(carla.LaneMarkingType.NONE, carla.LaneMarkingColor.Other, [])]
|
|
|
|
def draw_lane(surface, lane, color):
|
|
"""Renders a single lane in a surface and with a specified color"""
|
|
for side in lane:
|
|
lane_left_side = [lateral_shift(w.transform, -w.lane_width * 0.5) for w in side]
|
|
lane_right_side = [lateral_shift(w.transform, w.lane_width * 0.5) for w in side]
|
|
|
|
polygon = lane_left_side + [x for x in reversed(lane_right_side)]
|
|
polygon = [world_to_pixel(x) for x in polygon]
|
|
|
|
if len(polygon) > 2:
|
|
pygame.draw.polygon(surface, color, polygon, 5)
|
|
pygame.draw.polygon(surface, color, polygon)
|
|
|
|
def draw_lane_marking(surface, waypoints):
|
|
"""Draws the left and right side of lane markings"""
|
|
# Left Side
|
|
draw_lane_marking_single_side(surface, waypoints[0], -1)
|
|
|
|
# Right Side
|
|
draw_lane_marking_single_side(surface, waypoints[1], 1)
|
|
|
|
def draw_lane_marking_single_side(surface, waypoints, sign):
|
|
"""Draws the lane marking given a set of waypoints and decides whether drawing the right or left side of
|
|
the waypoint based on the sign parameter"""
|
|
lane_marking = None
|
|
|
|
marking_type = carla.LaneMarkingType.NONE
|
|
previous_marking_type = carla.LaneMarkingType.NONE
|
|
|
|
marking_color = carla.LaneMarkingColor.Other
|
|
previous_marking_color = carla.LaneMarkingColor.Other
|
|
|
|
markings_list = []
|
|
temp_waypoints = []
|
|
current_lane_marking = carla.LaneMarkingType.NONE
|
|
for sample in waypoints:
|
|
lane_marking = sample.left_lane_marking if sign < 0 else sample.right_lane_marking
|
|
|
|
if lane_marking is None:
|
|
continue
|
|
|
|
marking_type = lane_marking.type
|
|
marking_color = lane_marking.color
|
|
|
|
if current_lane_marking != marking_type:
|
|
# Get the list of lane markings to draw
|
|
markings = get_lane_markings(
|
|
previous_marking_type,
|
|
lane_marking_color_to_tango(previous_marking_color),
|
|
temp_waypoints,
|
|
sign)
|
|
current_lane_marking = marking_type
|
|
|
|
# Append each lane marking in the list
|
|
for marking in markings:
|
|
markings_list.append(marking)
|
|
|
|
temp_waypoints = temp_waypoints[-1:]
|
|
|
|
else:
|
|
temp_waypoints.append((sample))
|
|
previous_marking_type = marking_type
|
|
previous_marking_color = marking_color
|
|
|
|
# Add last marking
|
|
last_markings = get_lane_markings(
|
|
previous_marking_type,
|
|
lane_marking_color_to_tango(previous_marking_color),
|
|
temp_waypoints,
|
|
sign)
|
|
for marking in last_markings:
|
|
markings_list.append(marking)
|
|
|
|
# Once the lane markings have been simplified to Solid or Broken lines, we draw them
|
|
for markings in markings_list:
|
|
if markings[0] == carla.LaneMarkingType.Solid:
|
|
draw_solid_line(surface, markings[1], False, markings[2], 2)
|
|
elif markings[0] == carla.LaneMarkingType.Broken:
|
|
draw_broken_line(surface, markings[1], False, markings[2], 2)
|
|
|
|
def draw_arrow(surface, transform, color=COLOR_ALUMINIUM_2):
|
|
""" Draws an arrow with a specified color given a transform"""
|
|
transform.rotation.yaw += 180
|
|
forward = transform.get_forward_vector()
|
|
transform.rotation.yaw += 90
|
|
right_dir = transform.get_forward_vector()
|
|
end = transform.location
|
|
start = end - 2.0 * forward
|
|
right = start + 0.8 * forward + 0.4 * right_dir
|
|
left = start + 0.8 * forward - 0.4 * right_dir
|
|
|
|
# Draw lines
|
|
pygame.draw.lines(surface, color, False, [world_to_pixel(x) for x in [start, end]], 4)
|
|
pygame.draw.lines(surface, color, False, [world_to_pixel(x) for x in [left, start, right]], 4)
|
|
|
|
def draw_traffic_signs(surface, font_surface, actor, color=COLOR_ALUMINIUM_2, trigger_color=COLOR_PLUM_0):
|
|
"""Draw stop traffic signs and its bounding box if enabled"""
|
|
transform = actor.get_transform()
|
|
waypoint = carla_map.get_waypoint(transform.location)
|
|
|
|
angle = -waypoint.transform.rotation.yaw - 90.0
|
|
font_surface = pygame.transform.rotate(font_surface, angle)
|
|
pixel_pos = world_to_pixel(waypoint.transform.location)
|
|
offset = font_surface.get_rect(center=(pixel_pos[0], pixel_pos[1]))
|
|
surface.blit(font_surface, offset)
|
|
|
|
# Draw line in front of stop
|
|
forward_vector = carla.Location(waypoint.transform.get_forward_vector())
|
|
left_vector = carla.Location(-forward_vector.y, forward_vector.x,
|
|
forward_vector.z) * waypoint.lane_width / 2 * 0.7
|
|
|
|
line = [(waypoint.transform.location + (forward_vector * 1.5) + (left_vector)),
|
|
(waypoint.transform.location + (forward_vector * 1.5) - (left_vector))]
|
|
|
|
line_pixel = [world_to_pixel(p) for p in line]
|
|
pygame.draw.lines(surface, color, True, line_pixel, 2)
|
|
|
|
# Draw bounding box of the stop trigger
|
|
if self.show_triggers:
|
|
corners = Util.get_bounding_box(actor)
|
|
corners = [world_to_pixel(p) for p in corners]
|
|
pygame.draw.lines(surface, trigger_color, True, corners, 2)
|
|
|
|
# def draw_crosswalk(surface, transform=None, color=COLOR_ALUMINIUM_2):
|
|
# """Given two points A and B, draw white parallel lines from A to B"""
|
|
# a = carla.Location(0.0, 0.0, 0.0)
|
|
# b = carla.Location(10.0, 10.0, 0.0)
|
|
|
|
# ab = b - a
|
|
# length_ab = math.sqrt(ab.x**2 + ab.y**2)
|
|
# unit_ab = ab / length_ab
|
|
# unit_perp_ab = carla.Location(-unit_ab.y, unit_ab.x, 0.0)
|
|
|
|
# # Crosswalk lines params
|
|
# space_between_lines = 0.5
|
|
# line_width = 0.7
|
|
# line_height = 2
|
|
|
|
# current_length = 0
|
|
# while current_length < length_ab:
|
|
|
|
# center = a + unit_ab * current_length
|
|
|
|
# width_offset = unit_ab * line_width
|
|
# height_offset = unit_perp_ab * line_height
|
|
# list_point = [center - width_offset - height_offset,
|
|
# center + width_offset - height_offset,
|
|
# center + width_offset + height_offset,
|
|
# center - width_offset + height_offset]
|
|
|
|
# list_point = [world_to_pixel(p) for p in list_point]
|
|
# pygame.draw.polygon(surface, color, list_point)
|
|
# current_length += (line_width + space_between_lines) * 2
|
|
|
|
def lateral_shift(transform, shift):
|
|
"""Makes a lateral shift of the forward vector of a transform"""
|
|
transform.rotation.yaw += 90
|
|
return transform.location + shift * transform.get_forward_vector()
|
|
|
|
def draw_topology(carla_topology, index):
|
|
""" Draws traffic signs and the roads network with sidewalks, parking and shoulders by generating waypoints"""
|
|
topology = [x[index] for x in carla_topology]
|
|
topology = sorted(topology, key=lambda w: w.transform.location.z)
|
|
set_waypoints = []
|
|
for waypoint in topology:
|
|
waypoints = [waypoint]
|
|
|
|
# Generate waypoints of a road id. Stop when road id differs
|
|
nxt = waypoint.next(precision)
|
|
if len(nxt) > 0:
|
|
nxt = nxt[0]
|
|
while nxt.road_id == waypoint.road_id:
|
|
waypoints.append(nxt)
|
|
nxt = nxt.next(precision)
|
|
if len(nxt) > 0:
|
|
nxt = nxt[0]
|
|
else:
|
|
break
|
|
set_waypoints.append(waypoints)
|
|
|
|
# Draw Shoulders, Parkings and Sidewalks
|
|
PARKING_COLOR = COLOR_ALUMINIUM_4_5
|
|
SHOULDER_COLOR = COLOR_ALUMINIUM_5
|
|
SIDEWALK_COLOR = COLOR_ALUMINIUM_3
|
|
|
|
shoulder = [[], []]
|
|
parking = [[], []]
|
|
sidewalk = [[], []]
|
|
|
|
for w in waypoints:
|
|
# Classify lane types until there are no waypoints by going left
|
|
l = w.get_left_lane()
|
|
while l and l.lane_type != carla.LaneType.Driving:
|
|
|
|
if l.lane_type == carla.LaneType.Shoulder:
|
|
shoulder[0].append(l)
|
|
|
|
if l.lane_type == carla.LaneType.Parking:
|
|
parking[0].append(l)
|
|
|
|
if l.lane_type == carla.LaneType.Sidewalk:
|
|
sidewalk[0].append(l)
|
|
|
|
l = l.get_left_lane()
|
|
|
|
# Classify lane types until there are no waypoints by going right
|
|
r = w.get_right_lane()
|
|
while r and r.lane_type != carla.LaneType.Driving:
|
|
|
|
if r.lane_type == carla.LaneType.Shoulder:
|
|
shoulder[1].append(r)
|
|
|
|
if r.lane_type == carla.LaneType.Parking:
|
|
parking[1].append(r)
|
|
|
|
if r.lane_type == carla.LaneType.Sidewalk:
|
|
sidewalk[1].append(r)
|
|
|
|
r = r.get_right_lane()
|
|
|
|
# Draw classified lane types
|
|
draw_lane(map_surface, shoulder, SHOULDER_COLOR)
|
|
draw_lane(map_surface, parking, PARKING_COLOR)
|
|
draw_lane(map_surface, sidewalk, SIDEWALK_COLOR)
|
|
|
|
# Draw Roads
|
|
for waypoints in set_waypoints:
|
|
waypoint = waypoints[0]
|
|
road_left_side = [lateral_shift(w.transform, -w.lane_width * 0.5) for w in waypoints]
|
|
road_right_side = [lateral_shift(w.transform, w.lane_width * 0.5) for w in waypoints]
|
|
|
|
polygon = road_left_side + [x for x in reversed(road_right_side)]
|
|
polygon = [world_to_pixel(x) for x in polygon]
|
|
|
|
if len(polygon) > 2:
|
|
pygame.draw.polygon(map_surface, COLOR_ALUMINIUM_5, polygon, 5)
|
|
pygame.draw.polygon(map_surface, COLOR_ALUMINIUM_5, polygon)
|
|
|
|
# Draw Lane Markings and Arrows
|
|
if not waypoint.is_junction:
|
|
draw_lane_marking(map_surface, [waypoints, waypoints])
|
|
for n, wp in enumerate(waypoints):
|
|
if ((n + 1) % 400) == 0:
|
|
draw_arrow(map_surface, wp.transform)
|
|
|
|
topology = carla_map.get_topology()
|
|
draw_topology(topology, 0)
|
|
|
|
if self.show_spawn_points:
|
|
for sp in carla_map.get_spawn_points():
|
|
draw_arrow(map_surface, sp, color=COLOR_CHOCOLATE_0)
|
|
|
|
if self.show_connections:
|
|
dist = 1.5
|
|
|
|
def to_pixel(wp): return world_to_pixel(wp.transform.location)
|
|
for wp in carla_map.generate_waypoints(dist):
|
|
col = (0, 255, 255) if wp.is_junction else (0, 255, 0)
|
|
for nxt in wp.next(dist):
|
|
pygame.draw.line(map_surface, col, to_pixel(wp), to_pixel(nxt), 2)
|
|
if wp.lane_change & carla.LaneChange.Right:
|
|
r = wp.get_right_lane()
|
|
if r and r.lane_type == carla.LaneType.Driving:
|
|
pygame.draw.line(map_surface, col, to_pixel(wp), to_pixel(r), 2)
|
|
if wp.lane_change & carla.LaneChange.Left:
|
|
l = wp.get_left_lane()
|
|
if l and l.lane_type == carla.LaneType.Driving:
|
|
pygame.draw.line(map_surface, col, to_pixel(wp), to_pixel(l), 2)
|
|
|
|
actors = carla_world.get_actors()
|
|
|
|
# Find and Draw Traffic Signs: Stops and Yields
|
|
font_size = world_to_pixel_width(1)
|
|
font = pygame.font.SysFont('Arial', font_size, True)
|
|
|
|
stops = [actor for actor in actors if 'stop' in actor.type_id]
|
|
yields = [actor for actor in actors if 'yield' in actor.type_id]
|
|
|
|
stop_font_surface = font.render("STOP", False, COLOR_ALUMINIUM_2)
|
|
stop_font_surface = pygame.transform.scale(
|
|
stop_font_surface, (stop_font_surface.get_width(), stop_font_surface.get_height() * 2))
|
|
|
|
yield_font_surface = font.render("YIELD", False, COLOR_ALUMINIUM_2)
|
|
yield_font_surface = pygame.transform.scale(
|
|
yield_font_surface, (yield_font_surface.get_width(), yield_font_surface.get_height() * 2))
|
|
|
|
for ts_stop in stops:
|
|
draw_traffic_signs(map_surface, stop_font_surface, ts_stop, trigger_color=COLOR_SCARLET_RED_1)
|
|
|
|
for ts_yield in yields:
|
|
draw_traffic_signs(map_surface, yield_font_surface, ts_yield, trigger_color=COLOR_ORANGE_1)
|
|
|
|
def world_to_pixel(self, location, offset=(0, 0)):
|
|
"""Converts the world coordinates to pixel coordinates"""
|
|
x = self.scale * self._pixels_per_meter * (location.x - self._world_offset[0])
|
|
y = self.scale * self._pixels_per_meter * (location.y - self._world_offset[1])
|
|
return [int(x - offset[0]), int(y - offset[1])]
|
|
|
|
def world_to_pixel_width(self, width):
|
|
"""Converts the world units to pixel units"""
|
|
return int(self.scale * self._pixels_per_meter * width)
|
|
|
|
def scale_map(self, scale):
|
|
"""Scales the map surface"""
|
|
if scale != self.scale:
|
|
self.scale = scale
|
|
width = int(self.big_map_surface.get_width() * self.scale)
|
|
self.surface = pygame.transform.smoothscale(self.big_map_surface, (width, width))
|
|
|
|
|
|
class World(object):
|
|
"""Class that contains all the information of a carla world that is running on the server side"""
|
|
|
|
def __init__(self, name, args, timeout):
|
|
self.client = None
|
|
self.name = name
|
|
self.args = args
|
|
self.timeout = timeout
|
|
self.server_fps = 0.0
|
|
self.simulation_time = 0
|
|
self.server_clock = pygame.time.Clock()
|
|
|
|
# World data
|
|
self.world = None
|
|
self.town_map = None
|
|
self.actors_with_transforms = []
|
|
|
|
self._hud = None
|
|
self._input = None
|
|
|
|
self.surface_size = [0, 0]
|
|
self.prev_scaled_size = 0
|
|
self.scaled_size = 0
|
|
|
|
# Hero actor
|
|
self.hero_actor = None
|
|
self.spawned_hero = None
|
|
self.hero_transform = None
|
|
|
|
self.scale_offset = [0, 0]
|
|
|
|
self.vehicle_id_surface = None
|
|
self.result_surface = None
|
|
|
|
self.traffic_light_surfaces = TrafficLightSurfaces()
|
|
self.affected_traffic_light = None
|
|
|
|
# Map info
|
|
self.map_image = None
|
|
self.border_round_surface = None
|
|
self.original_surface_size = None
|
|
self.hero_surface = None
|
|
self.actors_surface = None
|
|
|
|
def _get_data_from_carla(self):
|
|
"""Retrieves the data from the server side"""
|
|
try:
|
|
self.client = carla.Client(self.args.host, self.args.port)
|
|
self.client.set_timeout(self.timeout)
|
|
|
|
if self.args.map is None:
|
|
world = self.client.get_world()
|
|
else:
|
|
world = self.client.load_world(self.args.map)
|
|
|
|
town_map = world.get_map()
|
|
return (world, town_map)
|
|
|
|
except RuntimeError as ex:
|
|
logging.error(ex)
|
|
exit_game()
|
|
|
|
def start(self, hud, input_control):
|
|
"""Build the map image, stores the needed modules and prepares rendering in Hero Mode"""
|
|
self.world, self.town_map = self._get_data_from_carla()
|
|
|
|
settings = self.world.get_settings()
|
|
settings.no_rendering_mode = self.args.no_rendering
|
|
self.world.apply_settings(settings)
|
|
|
|
# Create Surfaces
|
|
self.map_image = MapImage(
|
|
carla_world=self.world,
|
|
carla_map=self.town_map,
|
|
pixels_per_meter=PIXELS_PER_METER,
|
|
show_triggers=self.args.show_triggers,
|
|
show_connections=self.args.show_connections,
|
|
show_spawn_points=self.args.show_spawn_points)
|
|
|
|
self._hud = hud
|
|
self._input = input_control
|
|
|
|
self.original_surface_size = min(self._hud.dim[0], self._hud.dim[1])
|
|
self.surface_size = self.map_image.big_map_surface.get_width()
|
|
|
|
self.scaled_size = int(self.surface_size)
|
|
self.prev_scaled_size = int(self.surface_size)
|
|
|
|
# Render Actors
|
|
self.actors_surface = pygame.Surface((self.map_image.surface.get_width(), self.map_image.surface.get_height()))
|
|
self.actors_surface.set_colorkey(COLOR_BLACK)
|
|
|
|
self.vehicle_id_surface = pygame.Surface((self.surface_size, self.surface_size)).convert()
|
|
self.vehicle_id_surface.set_colorkey(COLOR_BLACK)
|
|
|
|
self.border_round_surface = pygame.Surface(self._hud.dim, pygame.SRCALPHA).convert()
|
|
self.border_round_surface.set_colorkey(COLOR_WHITE)
|
|
self.border_round_surface.fill(COLOR_BLACK)
|
|
|
|
# Used for Hero Mode, draws the map contained in a circle with white border
|
|
center_offset = (int(self._hud.dim[0] / 2), int(self._hud.dim[1] / 2))
|
|
pygame.draw.circle(self.border_round_surface, COLOR_ALUMINIUM_1, center_offset, int(self._hud.dim[1] / 2))
|
|
pygame.draw.circle(self.border_round_surface, COLOR_WHITE, center_offset, int((self._hud.dim[1] - 8) / 2))
|
|
|
|
scaled_original_size = self.original_surface_size * (1.0 / 0.9)
|
|
self.hero_surface = pygame.Surface((scaled_original_size, scaled_original_size)).convert()
|
|
|
|
self.result_surface = pygame.Surface((self.surface_size, self.surface_size)).convert()
|
|
self.result_surface.set_colorkey(COLOR_BLACK)
|
|
|
|
# Start hero mode by default
|
|
self.select_hero_actor()
|
|
self.hero_actor.set_autopilot(False)
|
|
self._input.wheel_offset = HERO_DEFAULT_SCALE
|
|
self._input.control = carla.VehicleControl()
|
|
|
|
# Register event for receiving server tick
|
|
weak_self = weakref.ref(self)
|
|
self.world.on_tick(lambda timestamp: World.on_world_tick(weak_self, timestamp))
|
|
|
|
def select_hero_actor(self):
|
|
"""Selects only one hero actor if there are more than one. If there are not any, it will spawn one."""
|
|
hero_vehicles = [actor for actor in self.world.get_actors()
|
|
if 'vehicle' in actor.type_id and actor.attributes['role_name'] == 'hero']
|
|
if len(hero_vehicles) > 0:
|
|
self.hero_actor = random.choice(hero_vehicles)
|
|
self.hero_transform = self.hero_actor.get_transform()
|
|
else:
|
|
self._spawn_hero()
|
|
|
|
def _spawn_hero(self):
|
|
"""Spawns the hero actor when the script runs"""
|
|
# Get a random blueprint.
|
|
blueprint = random.choice(self.world.get_blueprint_library().filter(self.args.filter))
|
|
blueprint.set_attribute('role_name', 'hero')
|
|
if blueprint.has_attribute('color'):
|
|
color = random.choice(blueprint.get_attribute('color').recommended_values)
|
|
blueprint.set_attribute('color', color)
|
|
# Spawn the player.
|
|
while self.hero_actor is None:
|
|
spawn_points = self.world.get_map().get_spawn_points()
|
|
spawn_point = random.choice(spawn_points) if spawn_points else carla.Transform()
|
|
self.hero_actor = self.world.try_spawn_actor(blueprint, spawn_point)
|
|
self.hero_transform = self.hero_actor.get_transform()
|
|
|
|
# Save it in order to destroy it when closing program
|
|
self.spawned_hero = self.hero_actor
|
|
|
|
def tick(self, clock):
|
|
"""Retrieves the actors for Hero and Map modes and updates de HUD based on that"""
|
|
actors = self.world.get_actors()
|
|
|
|
# We store the transforms also so that we avoid having transforms of
|
|
# previous tick and current tick when rendering them.
|
|
self.actors_with_transforms = [(actor, actor.get_transform()) for actor in actors]
|
|
if self.hero_actor is not None:
|
|
self.hero_transform = self.hero_actor.get_transform()
|
|
|
|
self.update_hud_info(clock)
|
|
|
|
def update_hud_info(self, clock):
|
|
"""Updates the HUD info regarding simulation, hero mode and whether there is a traffic light affecting the hero actor"""
|
|
|
|
hero_mode_text = []
|
|
if self.hero_actor is not None:
|
|
hero_speed = self.hero_actor.get_velocity()
|
|
hero_speed_text = 3.6 * math.sqrt(hero_speed.x ** 2 + hero_speed.y ** 2 + hero_speed.z ** 2)
|
|
|
|
affected_traffic_light_text = 'None'
|
|
if self.affected_traffic_light is not None:
|
|
state = self.affected_traffic_light.state
|
|
if state == carla.TrafficLightState.Green:
|
|
affected_traffic_light_text = 'GREEN'
|
|
elif state == carla.TrafficLightState.Yellow:
|
|
affected_traffic_light_text = 'YELLOW'
|
|
else:
|
|
affected_traffic_light_text = 'RED'
|
|
|
|
affected_speed_limit_text = self.hero_actor.get_speed_limit()
|
|
if math.isnan(affected_speed_limit_text):
|
|
affected_speed_limit_text = 0.0
|
|
hero_mode_text = [
|
|
'Hero Mode: ON',
|
|
'Hero ID: %7d' % self.hero_actor.id,
|
|
'Hero Vehicle: %14s' % get_actor_display_name(self.hero_actor, truncate=14),
|
|
'Hero Speed: %3d km/h' % hero_speed_text,
|
|
'Hero Affected by:',
|
|
' Traffic Light: %12s' % affected_traffic_light_text,
|
|
' Speed Limit: %3d km/h' % affected_speed_limit_text
|
|
]
|
|
else:
|
|
hero_mode_text = ['Hero Mode: OFF']
|
|
|
|
self.server_fps = self.server_clock.get_fps()
|
|
self.server_fps = 'inf' if self.server_fps == float('inf') else round(self.server_fps)
|
|
info_text = [
|
|
'Server: % 16s FPS' % self.server_fps,
|
|
'Client: % 16s FPS' % round(clock.get_fps()),
|
|
'Simulation Time: % 12s' % datetime.timedelta(seconds=int(self.simulation_time)),
|
|
'Map Name: %10s' % self.town_map.name,
|
|
]
|
|
|
|
self._hud.add_info(self.name, info_text)
|
|
self._hud.add_info('HERO', hero_mode_text)
|
|
|
|
@staticmethod
|
|
def on_world_tick(weak_self, timestamp):
|
|
"""Updates the server tick"""
|
|
self = weak_self()
|
|
if not self:
|
|
return
|
|
|
|
self.server_clock.tick()
|
|
self.server_fps = self.server_clock.get_fps()
|
|
self.simulation_time = timestamp.elapsed_seconds
|
|
|
|
def _show_nearby_vehicles(self, vehicles):
|
|
"""Shows nearby vehicles of the hero actor"""
|
|
info_text = []
|
|
if self.hero_actor is not None and len(vehicles) > 1:
|
|
location = self.hero_transform.location
|
|
vehicle_list = [x[0] for x in vehicles if x[0].id != self.hero_actor.id]
|
|
|
|
def distance(v): return location.distance(v.get_location())
|
|
for n, vehicle in enumerate(sorted(vehicle_list, key=distance)):
|
|
if n > 15:
|
|
break
|
|
vehicle_type = get_actor_display_name(vehicle, truncate=22)
|
|
info_text.append('% 5d %s' % (vehicle.id, vehicle_type))
|
|
self._hud.add_info('NEARBY VEHICLES', info_text)
|
|
|
|
def _split_actors(self):
|
|
"""Splits the retrieved actors by type id"""
|
|
vehicles = []
|
|
traffic_lights = []
|
|
speed_limits = []
|
|
walkers = []
|
|
|
|
for actor_with_transform in self.actors_with_transforms:
|
|
actor = actor_with_transform[0]
|
|
if 'vehicle' in actor.type_id:
|
|
vehicles.append(actor_with_transform)
|
|
elif 'traffic_light' in actor.type_id:
|
|
traffic_lights.append(actor_with_transform)
|
|
elif 'speed_limit' in actor.type_id:
|
|
speed_limits.append(actor_with_transform)
|
|
elif 'walker.pedestrian' in actor.type_id:
|
|
walkers.append(actor_with_transform)
|
|
|
|
return (vehicles, traffic_lights, speed_limits, walkers)
|
|
|
|
def _render_traffic_lights(self, surface, list_tl, world_to_pixel):
|
|
"""Renders the traffic lights and shows its triggers and bounding boxes if flags are enabled"""
|
|
self.affected_traffic_light = None
|
|
|
|
for tl in list_tl:
|
|
world_pos = tl.get_location()
|
|
pos = world_to_pixel(world_pos)
|
|
|
|
if self.args.show_triggers:
|
|
corners = Util.get_bounding_box(tl)
|
|
corners = [world_to_pixel(p) for p in corners]
|
|
pygame.draw.lines(surface, COLOR_BUTTER_1, True, corners, 2)
|
|
|
|
if self.hero_actor is not None:
|
|
corners = Util.get_bounding_box(tl)
|
|
corners = [world_to_pixel(p) for p in corners]
|
|
tl_t = tl.get_transform()
|
|
|
|
transformed_tv = tl_t.transform(tl.trigger_volume.location)
|
|
hero_location = self.hero_actor.get_location()
|
|
d = hero_location.distance(transformed_tv)
|
|
s = Util.length(tl.trigger_volume.extent) + Util.length(self.hero_actor.bounding_box.extent)
|
|
if (d <= s):
|
|
# Highlight traffic light
|
|
self.affected_traffic_light = tl
|
|
srf = self.traffic_light_surfaces.surfaces['h']
|
|
surface.blit(srf, srf.get_rect(center=pos))
|
|
|
|
srf = self.traffic_light_surfaces.surfaces[tl.state]
|
|
surface.blit(srf, srf.get_rect(center=pos))
|
|
|
|
def _render_speed_limits(self, surface, list_sl, world_to_pixel, world_to_pixel_width):
|
|
"""Renders the speed limits by drawing two concentric circles (outer is red and inner white) and a speed limit text"""
|
|
|
|
font_size = world_to_pixel_width(2)
|
|
radius = world_to_pixel_width(2)
|
|
font = pygame.font.SysFont('Arial', font_size)
|
|
|
|
for sl in list_sl:
|
|
|
|
x, y = world_to_pixel(sl.get_location())
|
|
|
|
# Render speed limit concentric circles
|
|
white_circle_radius = int(radius * 0.75)
|
|
|
|
pygame.draw.circle(surface, COLOR_SCARLET_RED_1, (x, y), radius)
|
|
pygame.draw.circle(surface, COLOR_ALUMINIUM_0, (x, y), white_circle_radius)
|
|
|
|
limit = sl.type_id.split('.')[2]
|
|
font_surface = font.render(limit, True, COLOR_ALUMINIUM_5)
|
|
|
|
if self.args.show_triggers:
|
|
corners = Util.get_bounding_box(sl)
|
|
corners = [world_to_pixel(p) for p in corners]
|
|
pygame.draw.lines(surface, COLOR_PLUM_2, True, corners, 2)
|
|
|
|
# Blit
|
|
if self.hero_actor is not None:
|
|
# In hero mode, Rotate font surface with respect to hero vehicle front
|
|
angle = -self.hero_transform.rotation.yaw - 90.0
|
|
font_surface = pygame.transform.rotate(font_surface, angle)
|
|
offset = font_surface.get_rect(center=(x, y))
|
|
surface.blit(font_surface, offset)
|
|
|
|
else:
|
|
# In map mode, there is no need to rotate the text of the speed limit
|
|
surface.blit(font_surface, (x - radius / 2, y - radius / 2))
|
|
|
|
def _render_walkers(self, surface, list_w, world_to_pixel):
|
|
"""Renders the walkers' bounding boxes"""
|
|
for w in list_w:
|
|
color = COLOR_PLUM_0
|
|
|
|
# Compute bounding box points
|
|
bb = w[0].bounding_box.extent
|
|
corners = [
|
|
carla.Location(x=-bb.x, y=-bb.y),
|
|
carla.Location(x=bb.x, y=-bb.y),
|
|
carla.Location(x=bb.x, y=bb.y),
|
|
carla.Location(x=-bb.x, y=bb.y)]
|
|
|
|
w[1].transform(corners)
|
|
corners = [world_to_pixel(p) for p in corners]
|
|
pygame.draw.polygon(surface, color, corners)
|
|
|
|
def _render_vehicles(self, surface, list_v, world_to_pixel):
|
|
"""Renders the vehicles' bounding boxes"""
|
|
for v in list_v:
|
|
color = COLOR_SKY_BLUE_0
|
|
if int(v[0].attributes['number_of_wheels']) == 2:
|
|
color = COLOR_CHOCOLATE_1
|
|
if v[0].attributes['role_name'] == 'hero':
|
|
color = COLOR_CHAMELEON_0
|
|
# Compute bounding box points
|
|
bb = v[0].bounding_box.extent
|
|
corners = [carla.Location(x=-bb.x, y=-bb.y),
|
|
carla.Location(x=bb.x - 0.8, y=-bb.y),
|
|
carla.Location(x=bb.x, y=0),
|
|
carla.Location(x=bb.x - 0.8, y=bb.y),
|
|
carla.Location(x=-bb.x, y=bb.y),
|
|
carla.Location(x=-bb.x, y=-bb.y)
|
|
]
|
|
v[1].transform(corners)
|
|
corners = [world_to_pixel(p) for p in corners]
|
|
pygame.draw.lines(surface, color, False, corners, int(math.ceil(4.0 * self.map_image.scale)))
|
|
|
|
def render_actors(self, surface, vehicles, traffic_lights, speed_limits, walkers):
|
|
"""Renders all the actors"""
|
|
# Static actors
|
|
self._render_traffic_lights(surface, [tl[0] for tl in traffic_lights], self.map_image.world_to_pixel)
|
|
self._render_speed_limits(surface, [sl[0] for sl in speed_limits], self.map_image.world_to_pixel,
|
|
self.map_image.world_to_pixel_width)
|
|
|
|
# Dynamic actors
|
|
self._render_vehicles(surface, vehicles, self.map_image.world_to_pixel)
|
|
self._render_walkers(surface, walkers, self.map_image.world_to_pixel)
|
|
|
|
def clip_surfaces(self, clipping_rect):
|
|
"""Used to improve perfomance. Clips the surfaces in order to render only the part of the surfaces that are going to be visible"""
|
|
self.actors_surface.set_clip(clipping_rect)
|
|
self.vehicle_id_surface.set_clip(clipping_rect)
|
|
self.result_surface.set_clip(clipping_rect)
|
|
|
|
def _compute_scale(self, scale_factor):
|
|
"""Based on the mouse wheel and mouse position, it will compute the scale and move the map so that it is zoomed in or out based on mouse position"""
|
|
m = self._input.mouse_pos
|
|
|
|
# Percentage of surface where mouse position is actually
|
|
px = (m[0] - self.scale_offset[0]) / float(self.prev_scaled_size)
|
|
py = (m[1] - self.scale_offset[1]) / float(self.prev_scaled_size)
|
|
|
|
# Offset will be the previously accumulated offset added with the
|
|
# difference of mouse positions in the old and new scales
|
|
diff_between_scales = ((float(self.prev_scaled_size) * px) - (float(self.scaled_size) * px),
|
|
(float(self.prev_scaled_size) * py) - (float(self.scaled_size) * py))
|
|
|
|
self.scale_offset = (self.scale_offset[0] + diff_between_scales[0],
|
|
self.scale_offset[1] + diff_between_scales[1])
|
|
|
|
# Update previous scale
|
|
self.prev_scaled_size = self.scaled_size
|
|
|
|
# Scale performed
|
|
self.map_image.scale_map(scale_factor)
|
|
|
|
def render(self, display):
|
|
"""Renders the map and all the actors in hero and map mode"""
|
|
if self.actors_with_transforms is None:
|
|
return
|
|
self.result_surface.fill(COLOR_BLACK)
|
|
|
|
# Split the actors by vehicle type id
|
|
vehicles, traffic_lights, speed_limits, walkers = self._split_actors()
|
|
|
|
# Zoom in and out
|
|
scale_factor = self._input.wheel_offset
|
|
self.scaled_size = int(self.map_image.width * scale_factor)
|
|
if self.scaled_size != self.prev_scaled_size:
|
|
self._compute_scale(scale_factor)
|
|
|
|
# Render Actors
|
|
self.actors_surface.fill(COLOR_BLACK)
|
|
self.render_actors(
|
|
self.actors_surface,
|
|
vehicles,
|
|
traffic_lights,
|
|
speed_limits,
|
|
walkers)
|
|
|
|
# Render Ids
|
|
self._hud.render_vehicles_ids(self.vehicle_id_surface, vehicles,
|
|
self.map_image.world_to_pixel, self.hero_actor, self.hero_transform)
|
|
# Show nearby actors from hero mode
|
|
self._show_nearby_vehicles(vehicles)
|
|
|
|
# Blit surfaces
|
|
surfaces = ((self.map_image.surface, (0, 0)),
|
|
(self.actors_surface, (0, 0)),
|
|
(self.vehicle_id_surface, (0, 0)),
|
|
)
|
|
|
|
angle = 0.0 if self.hero_actor is None else self.hero_transform.rotation.yaw + 90.0
|
|
self.traffic_light_surfaces.rotozoom(-angle, self.map_image.scale)
|
|
|
|
center_offset = (0, 0)
|
|
if self.hero_actor is not None:
|
|
# Hero Mode
|
|
hero_location_screen = self.map_image.world_to_pixel(self.hero_transform.location)
|
|
hero_front = self.hero_transform.get_forward_vector()
|
|
translation_offset = (hero_location_screen[0] - self.hero_surface.get_width() / 2 + hero_front.x * PIXELS_AHEAD_VEHICLE,
|
|
(hero_location_screen[1] - self.hero_surface.get_height() / 2 + hero_front.y * PIXELS_AHEAD_VEHICLE))
|
|
|
|
# Apply clipping rect
|
|
clipping_rect = pygame.Rect(translation_offset[0],
|
|
translation_offset[1],
|
|
self.hero_surface.get_width(),
|
|
self.hero_surface.get_height())
|
|
self.clip_surfaces(clipping_rect)
|
|
|
|
Util.blits(self.result_surface, surfaces)
|
|
|
|
self.border_round_surface.set_clip(clipping_rect)
|
|
|
|
self.hero_surface.fill(COLOR_ALUMINIUM_4)
|
|
self.hero_surface.blit(self.result_surface, (-translation_offset[0],
|
|
-translation_offset[1]))
|
|
|
|
rotated_result_surface = pygame.transform.rotozoom(self.hero_surface, angle, 0.9).convert()
|
|
|
|
center = (display.get_width() / 2, display.get_height() / 2)
|
|
rotation_pivot = rotated_result_surface.get_rect(center=center)
|
|
display.blit(rotated_result_surface, rotation_pivot)
|
|
|
|
display.blit(self.border_round_surface, (0, 0))
|
|
else:
|
|
# Map Mode
|
|
# Translation offset
|
|
translation_offset = (self._input.mouse_offset[0] * scale_factor + self.scale_offset[0],
|
|
self._input.mouse_offset[1] * scale_factor + self.scale_offset[1])
|
|
center_offset = (abs(display.get_width() - self.surface_size) / 2 * scale_factor, 0)
|
|
|
|
# Apply clipping rect
|
|
clipping_rect = pygame.Rect(-translation_offset[0] - center_offset[0], -translation_offset[1],
|
|
self._hud.dim[0], self._hud.dim[1])
|
|
self.clip_surfaces(clipping_rect)
|
|
Util.blits(self.result_surface, surfaces)
|
|
|
|
display.blit(self.result_surface, (translation_offset[0] + center_offset[0],
|
|
translation_offset[1]))
|
|
|
|
def destroy(self):
|
|
"""Destroy the hero actor when class instance is destroyed"""
|
|
if self.spawned_hero is not None:
|
|
self.spawned_hero.destroy()
|
|
|
|
# ==============================================================================
|
|
# -- Input -----------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
class InputControl(object):
|
|
"""Class that handles input received such as keyboard and mouse."""
|
|
|
|
def __init__(self, name):
|
|
"""Initializes input member variables when instance is created."""
|
|
self.name = name
|
|
self.mouse_pos = (0, 0)
|
|
self.mouse_offset = [0.0, 0.0]
|
|
self.wheel_offset = 0.1
|
|
self.wheel_amount = 0.025
|
|
self._steer_cache = 0.0
|
|
self.control = None
|
|
self._autopilot_enabled = False
|
|
|
|
# Modules that input will depend on
|
|
self._hud = None
|
|
self._world = None
|
|
|
|
def start(self, hud, world):
|
|
"""Assigns other initialized modules that input module needs."""
|
|
self._hud = hud
|
|
self._world = world
|
|
|
|
self._hud.notification("Press 'H' or '?' for help.", seconds=4.0)
|
|
|
|
def render(self, display):
|
|
"""Does nothing. Input module does not need render anything."""
|
|
|
|
def tick(self, clock):
|
|
"""Executed each frame. Calls method for parsing input."""
|
|
self.parse_input(clock)
|
|
|
|
def _parse_events(self):
|
|
"""Parses input events. These events are executed only once when pressing a key."""
|
|
self.mouse_pos = pygame.mouse.get_pos()
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
exit_game()
|
|
elif event.type == pygame.KEYUP:
|
|
if self._is_quit_shortcut(event.key):
|
|
exit_game()
|
|
elif event.key == K_h or (event.key == K_SLASH and pygame.key.get_mods() & KMOD_SHIFT):
|
|
self._hud.help.toggle()
|
|
elif event.key == K_TAB:
|
|
# Toggle between hero and map mode
|
|
if self._world.hero_actor is None:
|
|
self._world.select_hero_actor()
|
|
self.wheel_offset = HERO_DEFAULT_SCALE
|
|
self.control = carla.VehicleControl()
|
|
self._hud.notification('Hero Mode')
|
|
else:
|
|
self.wheel_offset = MAP_DEFAULT_SCALE
|
|
self.mouse_offset = [0, 0]
|
|
self.mouse_pos = [0, 0]
|
|
self._world.scale_offset = [0, 0]
|
|
self._world.hero_actor = None
|
|
self._hud.notification('Map Mode')
|
|
elif event.key == K_F1:
|
|
self._hud.show_info = not self._hud.show_info
|
|
elif event.key == K_i:
|
|
self._hud.show_actor_ids = not self._hud.show_actor_ids
|
|
elif isinstance(self.control, carla.VehicleControl):
|
|
if event.key == K_q:
|
|
self.control.gear = 1 if self.control.reverse else -1
|
|
elif event.key == K_m:
|
|
self.control.manual_gear_shift = not self.control.manual_gear_shift
|
|
self.control.gear = self._world.hero_actor.get_control().gear
|
|
self._hud.notification('%s Transmission' % (
|
|
'Manual' if self.control.manual_gear_shift else 'Automatic'))
|
|
elif self.control.manual_gear_shift and event.key == K_COMMA:
|
|
self.control.gear = max(-1, self.control.gear - 1)
|
|
elif self.control.manual_gear_shift and event.key == K_PERIOD:
|
|
self.control.gear = self.control.gear + 1
|
|
elif event.key == K_p:
|
|
# Toggle autopilot
|
|
if self._world.hero_actor is not None:
|
|
self._autopilot_enabled = not self._autopilot_enabled
|
|
self._world.hero_actor.set_autopilot(self._autopilot_enabled)
|
|
self._hud.notification('Autopilot %s' % ('On' if self._autopilot_enabled else 'Off'))
|
|
elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
# Handle mouse wheel for zooming in and out
|
|
if event.button == 4:
|
|
self.wheel_offset += self.wheel_amount
|
|
if self.wheel_offset >= 1.0:
|
|
self.wheel_offset = 1.0
|
|
elif event.button == 5:
|
|
self.wheel_offset -= self.wheel_amount
|
|
if self.wheel_offset <= 0.1:
|
|
self.wheel_offset = 0.1
|
|
|
|
def _parse_keys(self, milliseconds):
|
|
"""Parses keyboard input when keys are pressed"""
|
|
keys = pygame.key.get_pressed()
|
|
self.control.throttle = 1.0 if keys[K_UP] or keys[K_w] else 0.0
|
|
steer_increment = 5e-4 * milliseconds
|
|
if keys[K_LEFT] or keys[K_a]:
|
|
self._steer_cache -= steer_increment
|
|
elif keys[K_RIGHT] or keys[K_d]:
|
|
self._steer_cache += steer_increment
|
|
else:
|
|
self._steer_cache = 0.0
|
|
self._steer_cache = min(0.7, max(-0.7, self._steer_cache))
|
|
self.control.steer = round(self._steer_cache, 1)
|
|
self.control.brake = 1.0 if keys[K_DOWN] or keys[K_s] else 0.0
|
|
self.control.hand_brake = keys[K_SPACE]
|
|
|
|
def _parse_mouse(self):
|
|
"""Parses mouse input"""
|
|
if pygame.mouse.get_pressed()[0]:
|
|
x, y = pygame.mouse.get_pos()
|
|
self.mouse_offset[0] += (1.0 / self.wheel_offset) * (x - self.mouse_pos[0])
|
|
self.mouse_offset[1] += (1.0 / self.wheel_offset) * (y - self.mouse_pos[1])
|
|
self.mouse_pos = (x, y)
|
|
|
|
def parse_input(self, clock):
|
|
"""Parses the input, which is classified in keyboard events and mouse"""
|
|
self._parse_events()
|
|
self._parse_mouse()
|
|
if not self._autopilot_enabled:
|
|
if isinstance(self.control, carla.VehicleControl):
|
|
self._parse_keys(clock.get_time())
|
|
self.control.reverse = self.control.gear < 0
|
|
if (self._world.hero_actor is not None):
|
|
self._world.hero_actor.apply_control(self.control)
|
|
|
|
@staticmethod
|
|
def _is_quit_shortcut(key):
|
|
"""Returns True if one of the specified keys are pressed"""
|
|
return (key == K_ESCAPE) or (key == K_q and pygame.key.get_mods() & KMOD_CTRL)
|
|
|
|
|
|
# ==============================================================================
|
|
# -- Game Loop ---------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
def game_loop(args):
|
|
"""Initialized, Starts and runs all the needed modules for No Rendering Mode"""
|
|
try:
|
|
# Init Pygame
|
|
pygame.init()
|
|
display = pygame.display.set_mode(
|
|
(args.width, args.height),
|
|
pygame.HWSURFACE | pygame.DOUBLEBUF)
|
|
|
|
# Place a title to game window
|
|
pygame.display.set_caption(args.description)
|
|
|
|
# Show loading screen
|
|
font = pygame.font.Font(pygame.font.get_default_font(), 20)
|
|
text_surface = font.render('Rendering map...', True, COLOR_WHITE)
|
|
display.blit(text_surface, text_surface.get_rect(center=(args.width / 2, args.height / 2)))
|
|
pygame.display.flip()
|
|
|
|
# Init
|
|
input_control = InputControl(TITLE_INPUT)
|
|
hud = HUD(TITLE_HUD, args.width, args.height)
|
|
world = World(TITLE_WORLD, args, timeout=2.0)
|
|
|
|
# For each module, assign other modules that are going to be used inside that module
|
|
input_control.start(hud, world)
|
|
hud.start()
|
|
world.start(hud, input_control)
|
|
|
|
# Game loop
|
|
clock = pygame.time.Clock()
|
|
while True:
|
|
clock.tick_busy_loop(60)
|
|
|
|
# Tick all modules
|
|
world.tick(clock)
|
|
hud.tick(clock)
|
|
input_control.tick(clock)
|
|
|
|
# Render all modules
|
|
display.fill(COLOR_ALUMINIUM_4)
|
|
world.render(display)
|
|
hud.render(display)
|
|
input_control.render(display)
|
|
|
|
pygame.display.flip()
|
|
|
|
except KeyboardInterrupt:
|
|
print('\nCancelled by user. Bye!')
|
|
|
|
finally:
|
|
if world is not None:
|
|
world.destroy()
|
|
|
|
|
|
def exit_game():
|
|
"""Shuts down program and PyGame"""
|
|
pygame.quit()
|
|
sys.exit()
|
|
|
|
# ==============================================================================
|
|
# -- Main --------------------------------------------------------------------
|
|
# ==============================================================================
|
|
|
|
|
|
def main():
|
|
"""Parses the arguments received from commandline and runs the game loop"""
|
|
|
|
# Define arguments that will be received and parsed
|
|
argparser = argparse.ArgumentParser(
|
|
description='CARLA No Rendering Mode Visualizer')
|
|
argparser.add_argument(
|
|
'-v', '--verbose',
|
|
action='store_true',
|
|
dest='debug',
|
|
help='print debug information')
|
|
argparser.add_argument(
|
|
'--host',
|
|
metavar='H',
|
|
default='127.0.0.1',
|
|
help='IP of the host server (default: 127.0.0.1)')
|
|
argparser.add_argument(
|
|
'-p', '--port',
|
|
metavar='P',
|
|
default=2000,
|
|
type=int,
|
|
help='TCP port to listen to (default: 2000)')
|
|
argparser.add_argument(
|
|
'--res',
|
|
metavar='WIDTHxHEIGHT',
|
|
default='1280x720',
|
|
help='window resolution (default: 1280x720)')
|
|
argparser.add_argument(
|
|
'--filter',
|
|
metavar='PATTERN',
|
|
default='vehicle.*',
|
|
help='actor filter (default: "vehicle.*")')
|
|
argparser.add_argument(
|
|
'--map',
|
|
metavar='TOWN',
|
|
default=None,
|
|
help='start a new episode at the given TOWN')
|
|
argparser.add_argument(
|
|
'--no-rendering',
|
|
action='store_true',
|
|
help='switch off server rendering')
|
|
argparser.add_argument(
|
|
'--show-triggers',
|
|
action='store_true',
|
|
help='show trigger boxes of traffic signs')
|
|
argparser.add_argument(
|
|
'--show-connections',
|
|
action='store_true',
|
|
help='show waypoint connections')
|
|
argparser.add_argument(
|
|
'--show-spawn-points',
|
|
action='store_true',
|
|
help='show recommended spawn points')
|
|
|
|
# Parse arguments
|
|
args = argparser.parse_args()
|
|
args.description = argparser.description
|
|
args.width, args.height = [int(x) for x in args.res.split('x')]
|
|
|
|
# Print server information
|
|
log_level = logging.DEBUG if args.debug else logging.INFO
|
|
logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level)
|
|
|
|
logging.info('listening to server %s:%s', args.host, args.port)
|
|
print(__doc__)
|
|
|
|
# Run game loop
|
|
game_loop(args)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|