-
Notifications
You must be signed in to change notification settings - Fork 7
Guia de Colaboracao Criando um Atacante
Numa partida de VSSS o time que mais fizer ao final ganha, logo e natural que comecemos nosso time atraves da construcao de um atacante.
Nesse guia, usaremos os campos potenciais (algorithms.fields
) Para criar um atacante simples que:
- Se aproxima da bola por tras evitando colisoes com os demais robos
- Tenta se encaixar num angulo atras da bola adequado para conduzi-la para o gol
- conduz a bola ate o gol
Nossa ideia aqui nao devera fazer uma estrategia competitiva, apenas algo que possa servir de base para comparacao.
Para criarmos uma estrategia vamos primeiro entender os componentes mais importantes de uma classe Strategy
. Podemos fazer isso olhando a classe abstrata.
from abc import ABC
import controller
class Strategy(ABC):
# 1 metodo construtor
def __init__(self, match, name, controller=controller.SimpleLQR, controller_kwargs={}):
self.match = match
self._controller = controller
self._ctr_kwargs = controller_kwargs
self.name = name
# 2 metodo start
def start(self, robot=None):
'''
Inicializa a estrategia
'''
self.controller = self._controller(robot, **self._ctr_kwargs)
if robot:
self.robot = robot
def reset(self):
pass
def update(self):
return self.controller.update()
def set_desired(self, desired):
self.controller.set_desired(desired)
# 3 metodo decide
def decide(self):
'''
Calcula o proximo comando para o robo
retorna: vetor (x, y) objetivo
'''
return self.robot.x, self.robot.y
A classe Strategy
e a principal responsavel por encapsular nao apenas o algoritmo de path planning
que sera usado mas tambem o sistema de controle usado.
De forma resumida, os Algorithms
irao tomar decisoes usando os dados disponiveis pelo NeonFC, e no final, a Strategy
devera usar desses dados para retornar atraves do metodo decide uma velocidade desejada, essa velocidade devera ser enviada como (Vx, Vy)
. Porem, como o robo recebe as velocidades em potencias nas rodas esquerda e direita, toda Strategy
tambem tera um objeto Controller
. O Controller e responsavel em transformar a velocidade desejada para potencia das rodas. Dentro da teoria de controle existem diversas formas de se fazer isso, e para o NeonFC a sistema padrao e descrito na classe controller.SimpleLQR
. Nesse guia nao iremos nos aprofundar nos Controllers
e nao precisamos fazer nada caso optarmos por usar o controlador padrao.
Com isso em mente, vamos agora entender cada metodo na classe abstrata:
1 - construtor: O construtor de toda strategy recebe por padrao a referencia da match, uma string que representa o seu nome e dois parametros opcionais sobre controle. Vamos criar uma estrategia chamada SimpleAttacker
. Nosso construtor devera ficar assim:
from strategy.BaseStrategy import Strategy
class Attacker(Strategy):
def __init__(self, match):
super().__init__(match, "SimpleAttacker")
2 - start: No metodo start precisamos inicializar todo objeto que ira nos auxiliar na tomada de decisao. Aqui iremos instanciar todos os campos potenciais que iremos usar, por enquanto vamos apenas adicionar 3 campos potenciais vazios, dentro deles iremos adicionar outros campos, mas eles sera os nossos principais e que irao sendo trocados conforme necessidade.
Eles serao:
-
SeekBehaviour
: Campo potencial com a logica de perseguir a bola, de forma a evitar ficar preso nas paredes ou em outros robos -
AimBehaviour
: ASeekBehaviour
devera nos deixar atras da bola, aAimBehaviour
sera responsavel de apos disso posicionar o robo de forma de aproximar da bola de forma a "encaixar" nela direcionando ao gol adversario
Iremos construi-las e analisa-las separadamente para garantir que teremos o comportamento desejado, no final iremos trabalhar nas transicoes entre elas.
from strategy.BaseStrategy import Strategy
from algorithms.fields import PotentialField
class Attacker(Strategy):
def __init__(self, match):
super().__init__(match, "SimpleAttacker")
def start(self, robot=None):
super().start(robot=robot)
self.seek = PotentialField(self.match,name="SeekBehaviour")
self.aim = PotentialField(self.match,name="AimBehaviour")
3 - decide: Nesse metodo iremos efetivamente calcular a todo frame e enviar a velocidade (Vx, Vy)
. Aqui por enquanto apenas iremos colocar algumas atribuicoes comentadas para usar-los durante o desenvolvimento.
Aqui podemos perceber a chamada o metodo compute
pertencente a classe PotentialField
Ela recebe uma lista com [x, y]
e ira nos retornar nossa velocidade como (Vx, Vy)
.
from strategy.BaseStrategy import Strategy
from algorithms.fields import PotentialField
class Attacker(Strategy):
def __init__(self, match):
super().__init__(match, "SimpleAttacker")
def start(self, robot=None):
super().start(robot=robot)
self.seek = PotentialField(self.match,name="SeekBehaviour")
self.aim = PotentialField(self.match,name="AimBehaviour")
def decide(self):
return self.seek.compute([self.robot.x, self.robot.y])
# return self.aim.compute([self.robot.x, self.robot.y])
# return self.carry.compute([self.robot.x, self.robot.y])
Agora que construimos o esqueleto do nosso codigo, vamos salva-lo como strategy/tests/guideAttacker.py
. Nesse momento eh interessante que o leitor revise a pagina sobre campos potenciais.
Vamos iniciar com o comportante de seek! vamos tentar fazer o robo se aproxima da bola, se afastando dos adversarios.
from strategy.BaseStrategy import Strategy
from algorithms.fields import PotentialField, PointField
class Attacker(Strategy):
def __init__(self, match):
super().__init__(match, "SimpleAttacker")
def start(self, robot=None):
super().start(robot=robot)
self.seek = PotentialField(self.match,name="SeekBehaviour")
self.aim = PotentialField(self.match,name="AimBehaviour")
self.carry = PotentialField(self.match,name="CarryBehaviour")
self.seek.add_field(
PointField(
self.match,
target = lambda m : (m.ball.x, m.ball.y),
radius = .14,
decay = lambda x: x**2,
multiplier = .5
)
)
def decide(self):
return self.seek.compute([self.robot.x, self.robot.y])
# return self.aim.compute([self.robot.x, self.robot.y])
note que isso ja faz com que o robo se aproxime da bola com velocidade constante de 50 cm/s e comeca a desacelerar conforme ultrapassa 10 cm de distancia da bola. mas ainda nao conseguimos nos desviar dos robos adversarios, dessa forma, vamos criar campos de repulsao nos robos adversarios, de forma a evitar que nos choquemos a eles.
from strategy.BaseStrategy import Strategy
from algorithms.potential_fields.fields import PotentialField, PointField
class Attacker(Strategy):
def __init__(self, match):
super().__init__(match, "SimpleAttacker")
def start(self, robot=None):
super().start(robot=robot)
self.seek = PotentialField(self.match,name="SeekBehaviour")
self.aim = PotentialField(self.match,name="AimBehaviour")
self.seek.add_field(
PointField(
self.match,
target = lambda m : (m.ball.x, m.ball.y),
radius = .14,
decay = lambda x: x**2,
multiplier = .5
)
)
for robot in self.match.robots + self.match.opposites:
if robot.get_name() == self.robot.get_name():
continue
self.seek.add_field(
PointField(
self.match,
target = lambda m, r=robot: (
r.x,
r.y
),
radius = .15,
radius_max = .15,
decay = lambda x: 1,
multiplier = -.5
)
)
def decide(self):
return self.seek.compute([self.robot.x, self.robot.y])
# return self.aim.compute([self.robot.x, self.robot.y])
Aqui fizemos uma lista com self.match.robots + self.match.opposites
representando todos os robos em campo, e criamos um campo de repulsao para cada um deles (exceto o proprio robo).
Esta longe de ser uma estrategia perfeita, voce podera notar que vezes o robo fica preso proximo de algum obstaculo. Mas ja eh um comeco.
Agora, vamos ja pensar em deixar o robo numa boa posicao para transicionar desse comportamento para o comportamento de Aim
. Vamo fazer com que o robo se posicione atras da bola, inves de ir exatamente para ela, tambem vamos colocar um campo de repulsao na propria bola, isso vai evitar nosso robo de se aproximar da bola de forma a fazer gol contra.
self.seek.add_field(
PointField(
self.match,
target = lambda m : (max(m.ball.x - .10, .10), m.ball.y),
radius = .05,
decay = lambda x: x**4,
multiplier = .5
)
)
self.seek.add_field(
PointField(
self.match,
target = lambda m : (m.ball.x, m.ball.y),
radius = .075,
radius_max = .075,
decay = lambda x: 1,
multiplier = -.5
)
)
for robot in self.match.robots + self.match.opposites:
if robot.get_name() == self.robot.get_name():
continue
self.seek.add_field(
PointField(
self.match,
target = lambda m, r=robot: (
r.x,
r.y
),
radius = .15,
radius_max = .15,
decay = lambda x: 1,
multiplier = -.5
)
)
Agora, nosso robo (quase sempre) evita a bola e se posiciona atras dela, dessa forma, podemos trabalhar em melhorar essa estapa de ir para tras da bola, mas vamos encerrar o seek por agora para fins de simplicidade, antes de irmos para o Aim, vamos antes aprender a usar uma outra heranca para a classe Strategy
, a DebugPotentialFieldStrategy
que encapsula nela metodos que irao nos gerar uma mapa do campo de forma a nos permitir visualizar o campo potencial atuando no jogo.
Para fazermos isso precisamos mudar a classe mae da nossa estrategia:
from strategy import DebugTools
class Attacker(DebugTools.DebugPotentialFieldStrategy):
E tambem, no decide, deveremos chamar o decide da classe mae, dessa forma:
def decide(self):
return super().decide(self.seek)
isso para vermos o self.seek
. No futuro voce podera colocar tanto o aim quanto o carry.
Caso voce inicie o NeonFC com essa estrategia, voce ira notar que o robo nao ira se mexer, o que esta correto! esse processo demora um bocado, muito mais que o frame do jogo, entao o ideal e manter o robo parado, e o DebugPotentialFieldStrategy
faz isso para gente. Se voce rodar por alguns segundos, ja sera o suficiente para que o NeonFC tenha gerado um arquivo na raiz chamado ROBOT_N_color|potential_field.log
. Sendo N o id do robo rodando essa estrategia e color a cor desse robo. Esse arquivo e um log com o calculo do decide para todo o campo, podemos le-lo atraves de um Jupyter Notebook no repositorio do NeonFC, o notebooks/potential_fields_analysis.ipynb
. Voce pode derrubar o NeonFC uma vez que ver esse arquivo.
Nele poderemos gerar uma imagem clara de como o campo esta se comportando! Execute o Jupyter na raiz do NeonFC e siga as celulas passo a passo para ter uma imagem clara do campo potencial em questao!
Note a posicao que foi captada quando rodamos nossa estrategia usando o DebugPotentialFieldStrategy
:
Agora veja o grafico gerado pelo nosso notebook:
Essa ferramenta nos ajuda a visualizar nossas ideias e validar os algoritmos tambem, a cor representa a velocidade absoluta desejada, note que em alguns pontos ela se aproxima muito de zero, seria esse o motivo de algumas vezes nosso robo travar na frente de algum obstaculo?
Agora que temos a seek feita, precisamos criar um regra no decide, olhando para as variaveis que temos a nosso dispor para trocar de seek para aim, vamos trabalhar no decide agora, de forma a achar uma boa maneira de trocar.
def decide(self):
robot = self.robot
ball = self.match.ball
field = self.match.game.field.get_dimensions()
angle_ball_goal = math.atan2((field[1]/2 - ball.y), (field[0] + 0.2 - ball.x))
ball_to_goal = (
ball.x - math.cos(angle_ball_goal) * 0.2,
ball.y - math.sin(angle_ball_goal) * 0.2
)
is_near = lambda o, t, err: o - err <= t <= o + err
if self.behaviour == None:
self.behaviour = self.seek
elif self.behaviour == self.seek:
if (
is_near(robot.x, ball_to_goal[0], 0.04)
and is_near(robot.y, ball_to_goal[1], 0.05)):
self.behaviour = self.aim
else:
pass
elif self.behaviour == self.aim:
if robot.x >= self.match.ball.x:
self.behaviour = self.seek
print(self.behaviour.name)
#return super().decide(self.behaviour)
return self.behaviour.compute([self.robot.x, self.robot.y])
Agora o robo fica parado quando se aproxima, isso porque nao fizemos o self.aim
. Vamos fazer da seguinte forma: vamos encontrar um ponto exatamente atras da bola que descreva uma linha reta em direcao ao gol.
angle_ball_goal
e uma o angulo entre o robo e a bola, dessa forma conseguimos achar um ponto otimo, vamos tambem mudar o seek para inves de ser exatamente atras da bola, ser atras da bola com "angulo para o gol".
Nosso aim ficara assim:
def on_angle35(m, f=self.match.game.field):
field = f.get_dimensions()
angle_ball_goal = math.atan2((field[0] - m.ball.x), (field[1]/2 - m.ball.y))
ball_to_goal = (
m.ball.x - math.cos(angle_ball_goal) * 0.035,
m.ball.y - math.sin(angle_ball_goal) * 0.035
)
return ball_to_goal
self.aim.add_field(
PointField(
self.match,
target = on_angle35,
radius = .015,
decay = lambda x: x,
multiplier = .65
)
)
self.aim.add_field(
LineField(
self.match,
target = lambda m : (m.ball.x, m.ball.y),
theta = lambda m, f=self.match.game.field: math.atan2((f.get_dimensions()[0] - m.ball.x), (f.get_dimensions()[1]/2 - m.ball.y)) + math.pi/2,
line_size = .5,
line_size_single_size=True,
line_dist = .1,
line_dist_max = .1,
decay = lambda x: 1,
multiplier = .1
)
)
Aqui adicionamos dois campos, um de ponto, exatamente atras da bola, de forma que o robo sempre ira empurrar a bola quando tentar se aproxima dela, para evitar o robo de se aproximar da bola em outros angulos que nao em direcao do gol tambem iremos adicionar o campo de linha atras da bola de forma a forca o robo para o centro da bola.
Essa sequencia de campos potenciais podem ser dificeis de entender, voce pode plota-las tambem para facilitar o entendimento!
Por fim, nosso atacante ficara assim, alguns numeros foram adaptados para melhorar performance mas note que ele vive travando nas paredes e errando a bola quando ela esta em movimento, isso era esperado, nossa estrategia nao tenta evitar as paredes e em nenhum momento usamos a velocidade da bola para previsoes por exemplo, no geral nosso robo e lento entao sao coisas que podemos melhorar no futuro.
import math
from strategy.BaseStrategy import Strategy
from strategy.DebugTools import DebugPotentialFieldStrategy
from algorithms.potential_fields.fields import LineField, PotentialField, PointField
#class Attacker(DebugPotentialFieldStrategy):
class Attacker(Strategy):
def __init__(self, match):
super().__init__(match, "SimpleAttacker")
def start(self, robot=None):
super().start(robot=robot)
self.behaviour = None
self.seek = PotentialField(self.match,name="SeekBehaviour")
self.aim = PotentialField(self.match,name="AimBehaviour")
def on_angle20(m, f=self.match.game.field):
field = f.get_dimensions()
angle_ball_goal = math.atan2((field[1]/2 - m.ball.y), (field[0] - m.ball.x))
ball_to_goal = (
m.ball.x - math.cos(angle_ball_goal) * 0.2,
m.ball.y - math.sin(angle_ball_goal) * 0.2
)
return ball_to_goal
self.seek.add_field(
PointField(
self.match,
target = on_angle20,
radius = .05,
decay = lambda x: x**4,
multiplier = .75
)
)
self.seek.add_field(
PointField(
self.match,
target = lambda m : (m.ball.x, m.ball.y),
radius = .12,
radius_max = .12,
decay = lambda x: 1,
multiplier = -.75
)
)
for robot in self.match.robots + self.match.opposites:
if robot.get_name() == self.robot.get_name():
continue
self.seek.add_field(
PointField(
self.match,
target = lambda m, r=robot: (
r.x,
r.y
),
radius = .2,
radius_max = .2,
decay = lambda x: 1,
multiplier = -.75
)
)
def on_angle35(m, f=self.match.game.field):
field = f.get_dimensions()
angle_ball_goal = math.atan2((field[0] - m.ball.x), (field[1]/2 - m.ball.y))
ball_to_goal = (
m.ball.x - math.cos(angle_ball_goal) * 0.035,
m.ball.y - math.sin(angle_ball_goal) * 0.035
)
return ball_to_goal
self.aim.add_field(
PointField(
self.match,
target = on_angle35,
radius = .015,
decay = lambda x: x,
multiplier = .65
)
)
self.aim.add_field(
LineField(
self.match,
target = lambda m : (m.ball.x, m.ball.y),
theta = lambda m, f=self.match.game.field: math.atan2((f.get_dimensions()[0] - m.ball.x), (f.get_dimensions()[1]/2 - m.ball.y)) + math.pi/2,
line_size = .5,
line_size_single_size=True,
line_dist = .1,
line_dist_max = .1,
decay = lambda x: 1,
multiplier = .1
)
)
def decide(self):
robot = self.robot
ball = self.match.ball
field = self.match.game.field.get_dimensions()
angle_ball_goal = math.atan2((field[1]/2 - ball.y), (field[0] + 0.2 - ball.x))
ball_to_goal = (
ball.x - math.cos(angle_ball_goal) * 0.2,
ball.y - math.sin(angle_ball_goal) * 0.2
)
is_near = lambda o, t, err: o - err <= t <= o + err
if self.behaviour == None:
self.behaviour = self.seek
elif self.behaviour == self.seek:
if (
is_near(robot.x, ball_to_goal[0], 0.04)
and is_near(robot.y, ball_to_goal[1], 0.05)):
self.behaviour = self.aim
else:
pass
elif self.behaviour == self.aim:
if robot.x >= self.match.ball.x:
self.behaviour = self.seek
#return super().decide(self.behaviour)
return self.behaviour.compute([self.robot.x, self.robot.y])