Уроки Robocode Описание проекта Robocode Вы должны разработать маленького боевого робота и сдать код после экзаменов в середине семестра. Ваша задача: Как минимум класс, наследующийся от Robot или AdvancedRobot. Пожалуйста, используйте в названии робота часть своего имени. Например: "PingBot", "LanceAlot", "XhinStriker", "The Valinator", "AJ-D2", "RoboWendy" или что-то в этом роде. Код перемещения робота по полю боя. Код поиска других роботов (поворачивает радар), включая код реакции робота на обнаружение другого. Код поворота башни и стрельбы по противнику! (противники - ваши одноклассники) Что делать с роботом, когда код будет готов? На классных занятиях после экзаменов в середине семестра мы проделаем лабораторные работы и устроим большой "решающий поединок", где мы загрузим всех ваших роботов в компьютер вашего преподавателя с возможностью 3х выстрелов по 10 раундов в каждой битве. Эти битвы мы покажем всем на проекторе. (Ваша оценка не зависит от победы в бою, но все будут думать что вы слабак, если проиграете). Тем же вечером я хотел бы получить от Вас подписанную распечатку кода вашего робота. Урок #1: Мой первый робот Создание робота Здесь весёлая чепуха: из этого и состоит Robocode! Создание робота может быть легким, а вот превращение его в победителя - нет. Вы можете потратить на него несколько минут, а можете и долгие месяцы. Я предупреждаю вас, что написание робота может вызвать привыкание! Однажды начав вы будете наблюдать за тем, как ваше творение допускает ошибки и пропускает в себя попадания. Но научившись сами вы сможете научить своего робота многому: как действовать, что делать, куда двигаться, кого бояться и когда стрелять. Должен ли он прятаться в углу, или прыгать в пекло? Гляньте на робота с именем MatBot, одного из роботов автора Mathew Nelson'а. Мой первый робот Готовы создать своего первого робота? Я надеюсь что вы увлечетесь) Robocode содержит в себе несколько примеров простых роботов, глянув на которые вы сможете почерпнуть несколько идей и увидеть как все работает. Для этого можно использовать встроенный редактор (Robot Editor). В этом разделе мы будем использовать Robot Editor для создания вашего совственного нового робота. Встроенный редактор роботов (The Robot Editor) Перво-наперво откроем встроенный редактор Robot Editor. Из главного меню Robocode щелкните на пункте Robot menu, а затем выберите Editor. Когда появится окно редактора нажмите на пункт "File" главного меню и выберите пункт "New Robot" В следующем диалоге введите имя вашего робота и свои инициалы. Вуаля! Теперь вы видите код своего собственного робота. Новый робот Вот такое вы должны сейчас увидеть (все имена изменены для того чтобы защитить невиновных): package man; import robocode.*; public class MyFirstRobot extends Robot { public void run() { while (true) { ahead(100); turnGunRight(360); back(100); turnGunRight(360); } } public void onScannedRobot(ScannedRobotEvent e) { fire(1); } } Здесь мы заняты только наглой стрельбой ... вы нехотите что-нибудь поменять? Совсем немного, правда? Кстати, если вы НА САМОМ ДЕЛЕ заинтересованы остальным, прямо сейчас, я опишу об этом здесь: package man; import robocode.*; public class MyFirstRobot extends Robot { public void run() { } } import robocode.*; - Говорит Java что вы собираетесь использовать объекты Robocode в вашем роботе. public class MyFirstRobot extends Robot - "Объект, который я описываю здесь имеет тип Robot, а назван MyFirstRobot". public void run() { } - Игра вызывает ваш метод run() когда начинается бой { } "Фигурные скобки" ({}) группируют код вместе. Здесь например - группируют весь код относящийся к роботу. Давайте поедем куда-нибудь Давайте добавим пару строк, которые будут что-то делать. Сперва мы изучим метод run(): while (true) { ahead(100); turnGunRight(360); back(100); turnGunRight(360); } while(true) { } означает: "Пока условие true является истиной, делаем всё что между фигурными скобками { }". Т.к. true всегда равно true (серьезно?! ;-), написанное означает: "Повторяй код внутри фигурных скобок вечно". Итак, робот будет выполнять действия: 1. проехать вперёд на 100 пикселей 2. повернуть пушку вправо на 360 градусов 3. вернуться назад на 100 пикселей 4. повернуть пушку влево/назад на 360 градусов Робот будет продолжать так делать снова и снова, до тех пор пока не умрёт, точно так как описано в цикле while(true) Не так уж плохо, неправда ли? Огонь на поражение! Когда наш радар обнаруживает робота, мы хотим выстрелить: public void onScannedRobot(ScannedRobotEvent e) { fire(1); } Игра вызывает ваш метод onScannedRobot каждый раз, когда в поле зрения радара попадает робот. Вместе с этим событием она присылает вам много информации о нем: имя, сколько брони осталось, местонахождение, градус поворота, скорость и т.д. Ну раз уж это простой робот, то мы не собираемся смотреть на всю эту приблуду) Давайте просто стрелять! Компиляция вашего робота Сперва сохраните вашего робота, выбрав в главном меню File -> Save. Затем последуйте указаниям диалога. Теперь, откомпилируйте его выбрав пункт Компиляция(Compile) в меню Компилятор(Compiler). Если ваш робот скомпилировался без единой ошибки, то вы уже можете начать бой. Выберите в главном меню: Battle -> New. Если вы не можете увидеть вашего робота в списках, обновите списки нажатием Ctrl+R. Поставте вашего робота в список Selected Robots вместе с еще одним роботом, например sample.Target. Нажмите кнопку "Start Battle" и бой начнется! Урок №2: Поле сражения В этом урок емы рассмотрим начала перемещения по полю боя. Анатомия робота Ваш робот состоит из трех частей: корпус, пушка и радар. Картинка взята из "Rock 'em, sock 'em Robocode!" Все эти части могут двигаться независимо, для этого нужно вызвать методы setAdjustGunForRobotTurn(true) или setAdjustRadarForRobotTurn(true). Пример: AnatomyBot - показывает независимое управление этими тремя частями, к тому же показывает как выводить отладочные сообщения. (Щелкните мышкой по кнопке с именем робота справа от поля боя и увидите окошко с отладочными сообщениями.) Заметьте, что танк движется довольно медленно, башня поворачивается быстрее, а самым быстрым является радар. Координаты поля боя Поле боя представляет собой декартову плоскость с началом координат в нижнем левом углу. Отсчет градусов начинается сверху по часовой стрелке, таким образом в верхней точке это будет значение 0/360. Изображение взято из Rock 'em, sock 'em Robocode: Раунд 2 Пример робота: WallBanger - показывает как можно перемещаться по полю и врезаться в стены. Стоить заметить, что несмотря на вызов функции ahead(10000), onHitWall вызывается как только робот ударяется о стену. (Смотрите API робокода.) Пример бота "Walls" так же демонстрирует как робот может взаимодействовать со стенами поля битвы. Относительный угол Вы можете узнать свое абсолютное направление вызвав функцию getHeading(), но не путайте ее с функцией вызова относительного угла Bearing, который принимает значения от -180 до 180 и показывает смещение вашего направления от направления на что-либо (на стену, другого робота и т.д.). Используя относительный угол, вы можете легко повернуться к чему-нибудь передом (а к лесу задом :-)) к примеру: turnRight(event.getBearing()); Который является довольно общим выражением. Заметьте это потому, что относительный угол является величиной от -180 до 180, вызывая turnRight() на самом деле заставляет вас повернуться влево, если относительный угол отрицательный - так задумано и это именно то, что вы ожидаете. Пример робота: BearingBot - пример того как повернуть вашего робота вперёд в сторону врага и потаранить его. (Снова сопоставте его с другим роботом, передвижение которого слегка похоже на Tracker, SpinBot или Crazy) В качестве примера "RamFire" бот демонстрирует силу тарана. Урок #3: Основы сканирования В этом уроке мы опишем основы сканирования поля боя. Тема Вашей лабораторной работы №3 совпадает с содержимым этого Robocode-урока. Сенсоры робота Мы начнём этот урок с обсуждения органов чувств робота. Их у него немного. Осязание Ваш робот знает когда он: 1. врезается в стену (событие onHitWall) 2. попадание вражеской пули (событие onHitByBullet) 3. столкновение с другим роботом (событие onHitRobot) Все эти методы возвращают события, дающие Вам информацию о том, с чем столкнулся робот. Зрение Ваш робот знает когда замечен другой робот, но только если просканирует это (метод onScannedRobot) События сканирования, пожалуй, наиболее важные из всех событий. Они дают вам информацию о других роботах на поле сражения. (У некоторых роботов более 90% кода содержится именно в методе onScannedRobot). В сущности, единственный способ инициировать события сканирования - переместить ваш радар. (Если вражеский робот появится перед вашим радаром - он инициирует сканирующее событие, но вам всё же следует быть более проактивным.) К тому же вспомните урок 2 - сканер это самая быстрая из движущихся частей вашего робота, так что не ленитесь двигать ей. Если хотите, можете сделать видимыми линии направления радара, выбрав пункт Options Menu -> Preferences -> View Options Tab и поставив галку на "Visible Scan Arcs". Это пригодится во время отладки. Другие чувства Ваш робот также знает, когда он погибает (метод onDeath), знает, когда погибает другой робот (onRobotDeath - этот метод мы используем сегодня) и когда он выигрывает раунд (метод onWin - здесь Вы описываете Ваш победный танец). Ваш робот также следит за своими пулями и знает, когда его пуля поразила вражеского робота (событие onBulletHit) Построение робота получше Вот несколько простых сканирующих перемещений. Отметьте себе ещё раз, что мы вызываем setAdjustRadarForRobotTurn(true), чтобы иметь независимое перемещение радара. Последовательные перемещения с классом Robot Вы хотите найти других роботов, чтобы уничтожить их. Чтобы сделать это, Вам необходимо просканировать поле на наличие других роботов. Проще всего это достигается сканированием и вращением радара по кругу. Рабочий метод может выглядеть следующим образом: public void run() { setAdjustRadarForRobotTurn(true); while (true) { turnRadarRight(360); } } Действительно, наш любимый BearingBot с занятий на прошлой недели делал в точности следующее: он вращал свой радар и передвигался ближе к облучаемому объекту. Вы можете заметить, что у робота BearingBot есть большой недостаток, который явно будет виден, если выставить против него робота, который часто перемещается по полю боя (например робот Crazy). BearingBot облучает вражеского робота и перемещается в то место, где тот был только что зафиксирован радаром, но к моменту, пока он достигнет намеченной цели - оппонент уже переместится в другое место. Сложные перемещения с классом AdvancedRobot Было бы здорово, если бы мы могли делать несколько действий одновременно (сканировать, поворачивать и стрелять). К счастью, власть имущие предоставили нам средство достигнуть этого: базовый класс AdvancedRobot , позволяющий нам совершать неблокирующие вызовы для перемещения робота и выполнять их все как составные действия. Примерами улучшенных роботов являются Crazy и SpinBot (и. как это ни странно, SittingDuck). Чтобы улучшить Вашего робота, просто измените объявление класса с: class MyRobot extends Robot { ... на class MyRobot extends AdvancedRobot { ... Теперь Ваш класс наследует свойства класса AdvancedRobot, а не класса Robot. Пример робота: AdvancedBearingBot - замечательное продолжение робота 'BearingBot', потому что он может выполнять составные действия. Пример робота: AdvancedTracker - это модификация робота 'Tracker'. Заметьте, насколько исходный код робота 'Tracker' был улучшен, после перевода его на класс AdvancedRobot. Важное замечание: если Вы делаете робота, производным от класса AdvancedRobot, Вам следует вызывать метод execute(), чтобы выполнить все действия в очереди. Но робот AdvancedBearingBot обладает другим крупным недостатком, который можно увидеть, если выставить против него более, чем одного оппонента: он будет мотаться по полю, преследуя одного робота за другим и не догонит ни одного А всё потому, что его радар продолжает сканировать поле боя и он преследует всех, кого там зафиксировал. Короче говоря, он не может определиться с целью. Захват врага Узкий луч Мы можем очень просто захватить врага, постоянно направляя на него радар, где бы он не находился. <...> public void onScannedRobot(ScannedRobotEvent e) { // Захватываем нашу цель (я надеюсь) setTurnRadarRight(e.getBearing()); ... С этим есть проблема. Несмотря на то, что событие ScannedRobotEvent дает нам направление на просканированного робота, но это положение врага относительно нашего робота, а не относительно радара. Как бы нам решить эту небольшую неувязочку? Очень просто! Мы находим разность между положением нашего робота (метод getHeading()) и текущим положением радара (метод getRadarHeading()) и добавляем пеленг на просканированного робота (метод e.getBearing()), вот таким образом: public void onScannedRobot(ScannedRobotEvent e) { // Захватываем нашу цель (на этот раз уж точно) setTurnRadarRight(getHeading() - getRadarHeading() + e.getBearing()); ... Пример робота: NarrowBeam - использует вышеупомянутый метод, чтобы захватить вражеского робота и прибить его. Можете выставлять против него любого оппонента на Ваш вкус. Решение одной проблемы приводит к созданию другой. Решив здесь проблему захвата цели мы столкнулись с другой, которую иллюстрирует этот скриншот: Смотрите - Narrow Beam так зациклился на Crazy, что не замечает как Tracker убивает его сзади. Колебание (или "Пошатование") радара Согласно этой технике, каждый раз, когда Вы видите оппонента, Вы поворачиваете радар обратно для того, что бы сфокусироваться на одном роботе и периодически генерировать сканирующие события. Это является шагом вперед по сравнению с узким направленным лучом, потому что Вы в большей степени осведомлены о ближайших роботах. Для реализации этого Вам необходима переменная, в которой хранится направление поворота радара. Она всегда принимает значения 1 или -1, поэтому будет занимать совсем мало памяти. Вы может объявить её в своем роботе следующим образом: class MyRobot extends AdvancedRobot { private byte scanDirection = 1; ... Метод run можно не изменять, но onScannedRobot нужно дополнить следующим: public void onScannedRobot(ScannedRobotEvent e) { ... scanDirection *= -1; // changes value from 1 to -1 setTurnRadarRight(360 * scanDirection); ... Переключение значения переменной scanDirection создаёт эффект колебания. Пример робота: Oscillator - колеблет радаром для того чтобы отслеживать движение врага. Стоит заметить, что пока он отслеживает одного врага, он гоняется также и за теми кто ближе. Но все-же мы еще не решили проблему NarrowBeam до конца: другие роботы все еще могут зайти за спину Oscillator'у и уничтожить его. К тому-же, Oscillator имеет тенденцию иногда терять фокус. Ведение цели Используя класс EnemyBot Дальнейшее улучшение, которое может быть сделано - это выделить отдельного робота, сфокусироваться на нем и полностью его уничтожить. У Tracker'а это выходит неважно, но мы сделаем лучший экземпляр, используя класс EnemyBot. Чтобы следить за информацией о вражеской машине, вы должны создать переменную-член в классе вашего робота. Приблизительно так: class MyRobot extends AdvancedRobot { private EnemyBot enemy = new EnemyBot(); ... Вам понадобится очистить переменную врага в самом начале метода run: public void run() { setAdjustRadarForRobotTurn(true); enemy.reset(); while (true) { setTurnRadarRight(360); ... execute(); } } Также Вам потребуется обновить в методе onScannedRobot информацию о противнике, следующим образом: public void onScannedRobot(ScannedRobotEvent e) { if (enemy.none() || e.getName().equals(enemy.getName())) { enemy.update(e); } ... Начиная с этого момента вы можете использовать всю информацию о враге в любых других методах вашего класса. Последняя деталь. Даже если враг, за которым вы следите умер, вы все равно должны очистить переменную enemy, чтобы иметь возможность следить за другим вражеским ботом. public void onRobotDeath(RobotDeathEvent e) { if (e.getName().equals(enemy.getName())) { enemy.reset(); } } EnemyTracker - это робот, который использует класс EnemyBot. Заметьте, несмотря на то, что EnemyTracker поворачивает радар, он всегда следит за одним врагом. Поворот радара позволяет ему не терять из виду то, что делается на остальной части поля и в тоже время концентрироватся на его цели. Чуть более умная слежка Другое небольшое улучшение может быть сделано в следующем. Если более близкий робот попал в радиус обзора, возможно мы захотитм стрелять по нему, вместо текущей цели. Вы можете завершить это изменив метод onScannedRobot следующим образом public void onScannedRobot(ScannedRobotEvent e) { if ( // врага нет или enemy.none() || // тот, которого мы заметили находится близко, или e.getDistance() < enemy.getDistance() || // мы нашли того, за кем следили e.getName().equals(enemy.getName()) ){ // преследовать его enemy.update(e); } ... EnemyOrCloser использует алгорим сканирования приведенный выше, чтобы стрелять по ближайшему врагу. Так как он вращает свой радар, он начнет следить за любым врагом, который окажется еще ближе (даже если этот враг подкратдется сзади его). Урок #4: Основы стрельбы В этом уроке мы опишем основы вращения и нацеливания вашей пушки. Некоторый материал становиться сложноватым, но я думаю, что вы ребята справитесь с этим. Ваше упражнение в 4-ой лабораторной работе соответствует этому уроку. Готов! Целься! Пли! Независимое перемещение пушки На прошлой неделе мы действительно продвинулсь вперед с роботами типа Oscillator и EnemyTracker, но у них есть крупный недостаток: все они должны повернутся к своему врагу, чтобы выстрелить в него. Как мы помним из урока 2, наш робот состоит из 3-х частей, которые могут двигатся независимо, и, кстати, корпус танка делает это наиболее медленно Улучшение, которое можно сделать, заключается в раздельном движении корпуса и пушки. Это можно легко сделать, вызвав метод setAdjustGunForRobotTurn(true). После этого вы можете использовать методы типа turnGunRight() (или, предпочтительней, setTurnGunRight()) чтобы вращать орудие в стороны независимо от самого танка. Теперь вы можете поворачивать пушку в одну сторону, а ехать в другую. Простая формула прицеливания Теперь мы можем легко повернуть орудие прямо на обнаруженного врага, используя формулу, сходную с "узким лучем", рассмотренном на прошлой неделе. Мы находили разницу между направлением танка (getHeading()) и направлением его орудия (getGunHeading()) и складывали с относительным положением цели (getBearing()). Вот так: setTurnGunRight(getHeading() - getGunHeading() + e.getBearing()); Формула для расчёта огневой мощи Другая важная сторона стрельбы - это вычисление огневой мощи вашего снаряда. Документация для метода fire() объясняет то, что вы можете выстрелить снаряд в диапазоне от 0.1 до 3.0. Возможно вы уже сделали заключение о том, что является хорошей идеей стрелять снарядами слабой мощности когда ваш враг далеко и сильной мощностью когда он близко. Вы можете использовать конструкцию из операторов if-else-if-else чтобы определить силу выстрела, основываясь на том, находится ли враг на расстоянии 100 пикселей, 200 или более. Но такие конструкции достаточно громоздки и негибки. В конце концов, область возможных значений стремится к бесконечности, она не разделена на отдельные блоки. Лучшее решение - это использовать формулу. Вот пример: setFire(400 / enemy.getDistance()); Исходя из этой формулы: как только расстояние до врага увеличивается, сила выстрела уменьшается. И наоборот: как только враг подбирается ближе, мы стреляем сильнее. Значения, большие чем 3 будут уменьшаться, так что мы никогда не будем стрелять пулями, мощность которых превосходит 3, но мы все-таки должны уменьшать передаваемое в fire значение (для безопасности). Например: setFire(Math.min(400 / enemy.getDistance(), 3)); Пример робота. Shooter - это робот, особенность которого - независимое движение пушки. Также он использует обе приведенные выше формулы для того, чтобы стрелять по врагу. Сравните его с SittingDuck, Target, Fire, TrackFire, Corners и может, даже с Tracker и понаблюдайте за его поворотами и выстрелами. Более эффективное прицелевание Как вы, возможно заметили, у Shooter'а есть проблема: иногда он поворачивает свою пушку на врага по более длинному пути, чем следовало бы. (А иногда он просто стоит на месте, вращая ею). Наихудший случай - он может повернуть орудие на 359 градусов чтобы ударить по врагу, который находится всего-лишь на 1 градус правее или левее. Нормализованое направление Проблема в том, что направление удара, получаемое по вышеприведенной формуле не всегда нормализовано. Нормализованое направление (такое, как вы получаете из ScannedRobotEvent) - это направление между -180 и +180 градусами, как изображено на следующей иллюстрации: Не нормализованое направление может быть меньше чем -180 или больше чем 180. Мы предпочитаем работать с нормализованными направлениями, потому что они делают движения более эффективными. Чтоб нормализовать направление, используйте следующую функцию: // нормализует относительный угол между +180 и -180 double normalizeBearing(double angle) { while (angle > 180) angle -= 360; while (angle < -180) angle += 360; return angle; } Заметьте, что использование оператора while предпочтительней, чем if, в случаях, когда входящие углы либо очень велики, либо очень малы. (While - это тот же if, исключаяя то, что он - цикл.) Пример робота. NormalizedShooter - это робот, который нормализует движение пушки используя вышеприведенную функцию. Избежание необдуманых выстрелов Проблема Shooter и NormalizedShooter заключается в том, что они могут стрелять до того, как их орудие повернется на цель до конца. Даже после того, как вы нормализуете направление, ваш робот все еще будет стрелять иногда нерационально. Чтобы избежать нерациональных выстрелов, вызовите метод getGunTurnRemaining() чтобы увидеть, насколько далека ваша пушка от цели и не производить выстрелов до тех пор, пока орудие не будет направлено на цель. Еще вы не можете стрелять, когда пушка "горячая", и попытка выстрелить всего лишь потратит ход. Мы можем узнать, охладилось ли орудие, вызвав getGunHeat(). Этот фрагмент кода реализует оба улучшения: // если пушка холодная и мы нацелены на мишень, стреляем! if (getGunHeat() == 0 && Math.abs(getGunTurnRemaining()) < 10) setFire(firePower); Не бойтесь поексперементировать со значениями, отличными от 10. EfficientShooter использует функцию normalizeBearing для более эффективных повротов. Он избегает нерациональных выстрелов используя условие, приведенное выше. Отклонимся от темы: абсолютное направление. В противоположность относительному, абсолютное направление принимает значения между 0 и 360 градусами. Следующая илюстрация демонстриует относительное и абсолютные направления от одного робота к другому: Абсолютное направление часто бывает полезным. Можно получить абсолютное направление из относительного в классе AdvancedEnemyBot и получить x и y координаты врага. Другое применение абсолютного направления это получения угла между двумя произвольными точками. Следующая функця сделает это за вас :) // расчет относительного направления между 2-мя точками double absoluteBearing(double x1, double y1, double x2, double y2) { double xo = x2-x1; double yo = y2-y1; double hyp = Point2D.distance(x1, y1, x2, y2); double arcSin = Math.toDegrees(Math.asin(xo / hyp)); double bearing = 0; if (xo > 0 && yo > 0) { // обе позиции: внизу слева bearing = arcSin; } else if (xo < 0 && yo > 0) { // x отриц., y полож.: внизу справа bearing = 360 + arcSin; // arcsin здесь отрицательный, реально 360 - ang } else if (xo > 0 && yo < 0) { // x полож., y отриц: вверху слева bearing = 180 - arcSin; } else if (xo < 0 && yo < 0) { // оба отриц.: вверху слева bearing = 180 - arcSin; // arcsin отрицательный, реально 180 + ang } return bearing; } RunToCenter - это робот, который может двигатся из любого места в центр поля, высчитываея абсолютное направление между своим положением и точкой центра. Заметьте, он нормализует абсолютное направление (вызовом normalizeBearing) для более эффективного поворота. Посмотрите на его сражение с Walls и вы увидите, как один из ботов занимает углы, а другой становится в центре Прогнозируемое прицеливание Или "Использование тригонометрии, чтобы впечатлить своих друзей и уничтожить врагов" Осталась одна проблема: если Вы посмотрите, как предыдущие роботы обходились с роботом Walls, то вы увидите, что они постоянно промахивались. Причина этой проблемы *(кроется в тупости стрельбы без упреждения)* была в том, что пуля пролетает определённое расстояние, прежде чем поразит цель, а это занимает время, за которое цель могла сместиться в любую сторону с момента выстрела. Если вы хотим побить робота Walls (или любого другого робота), чаще всего, нам придётся прогнозировать куда он переместится после выстрела, но как это сделать? Перемещение тела равно скорости тела, умноженной на время, за которое тело проходит этот путь. Используя эту незамысловатую формулу, известную нам ещё с седьмого класса, мы можем определить сколько времени займёт полёт пули к цели. * Расстояние до врага может быть определено при помощи вызова метода enemy.getDistance() * Скорость пули, на основании Robocode FAQ равна [ 20 - три огневой мощи снаряда ] * Время мы можем вычислить по формуле [1] Код, приведённый ниже, как раз и производит такое вычисление: // вычисляем огневую мощь, основываясь на расстоянии double firePower = Math.min(500 / enemy.getDistance(), 3); // вычисляем скорость снаряда double bulletSpeed = 20 - firePower * 3; // S = V*t, определяем время long time = (long)(enemy.getDistance() / bulletSpeed); Получение будущих координат противника Далее, используем класс AdvancedEnemyBot, который не так давно написали. Он содержит методы getFutureX() и getFutureY(). Чтобы начать использовать новые возможности класса, нам необходимо изменить код с: public class Shooter extends AdvancedRobot { private EnemyBot enemy = new EnemyBot(); на: public class Shooter extends AdvancedRobot { private AdvancedEnemyBot enemy = new AdvancedEnemyBot(); Затем, в методе onScannedRobot() , нужно изменить код с: public void onScannedRobot(ScannedRobotEvent e) { // следим, если у нас нет врага, тот, которого мы нашли, значительно // ближе или мы сканировали того, за которым следили if ( enemy.none() || e.getDistance() < enemy.getDistance() - 70 || e.getName().equals(enemy.getName())) { // следим за ним enemy.update(e); } ... на: public void onScannedRobot(ScannedRobotEvent e) { // смотрим, есть ли враг вообще, или тот кого мы только что увидили значительно // ближе, или мы нашли того, за кем охотимся if ( enemy.none() || e.getDistance() < enemy.getDistance() - 70 || e.getName().equals(enemy.getName())) { // следовать за ним используя новый(NEW) метод обновления enemy.update(e, this); } ... Внимательный студент заметит, что вполне возможно использовать старый метод update, что приведет к нежелательным результатам. Будьте внимательны. Поворот орудия в нужную точку. В итоге мы получаем абсолютное направление между танком и ожидаемым положением врага, используя функцию absoluteBearing. Далее мы находим разность между абсолютным направлением и текущим положением пушки и поворачиваем орудие орудие таким образом, чтобы оно прошло наименьший путь. // расчет поворота пушки в определенное положение x,y double futureX = enemy.getFutureX(time); double futureY = enemy.getFutureY(time); double absDeg = absoluteBearing(getX(), getY(), futureX, futureY); // поворот орудия в расчитанное положение. setTurnGunRight(normalizeBearing(absDeg - getGunHeading())); PredictiveShooter использует функции описанные выше чтобы предвидеть, где может быть враг. Поставьте его против Walls и посмотрите что будет. Это просто уличная магия. :) Урок 5. Основы движения Это ваш последний урок, и чтоб обьеденить все вышеуслышанное, вернемся к уроку 2, где мы начали двигать нашего робота. Сейчас мы призовем его сюда, чтобы показать вам, как приближаться к вашему врагу, укланятся от пуль и избегать стен. Размышлизм. Если вы когда-нибудь играли в баскетбол, вы знаете следующее. Если нужно защитить кого-то, кто ведет мяч, то необходимо максимально ускорить свое боковое движение и стоять впереди него (стрейфится). Это работает и для ваших роботов, взгляните на илюстрацию: Желтый робот может легко двигатся из стороны в сторону, чтобы уклонятся от выстрелов голубого. У голубого же робота нет возможности уйти от опасности: если он двинется назад, то в него попадет снаряд, если же он попробует пойти вперед на таран, желтый бот сможет легко увернутся. Принятие боевой стойки Чтобы стать в выгодную позицию против оппонента, используйте следующий код: setTurnRight(enemy.getBearing() + 90); который всегда расположит вашего робота перпендикулярно врагу. Вперёд или назад? Когда вы уже встали в нужное положение относительно оппонента, вопрос, куда двигатся становится тривиальным. Вам возможно, более привычней слышать термины "стрейф влево" или "стрейф вправо". Чтобы держать под контролем направление движения просто обьявите новую переменну (как мы делали для колебания радара) class MyRobot extends AdvancedRobot { private byte moveDirection = 1; теперь, когда вы хотите сдвинуть своего бота, скажите заклинание: setAhead(100 * moveDirection); Вы можете переключать направление, просто поменяв знак moveDirection: moveDirection *= -1; Изменение направления движения Наиболее очевидное решение для переключения направления - это разворот на 180 градусов каждый раз, когда вы врезаетесь в стенку или другого робота. Вот так: public void onHitWall(HitWallEvent e) { moveDirection *= -1; } public void onHitRobot(HitRobotEvent e) { moveDirection *= -1; } Тем не менее, используя это, вы можете оказатся в плохом положении, упрямо нажимая на робота, который таранит вас с одной стороны (как собака в жару). Это происходит потому что onHitRobot() вызывается так много раз, что moveDirection будет переключатся постоянно и вы никогда не уедете оттуда. Лучший выход - это проверить, не остановился ли ваш робот. Если это случилось, то может означать только одно - вы куда-то врезались и нужно переключить направление. Это можно сделать следующим образом: if (getVelocity() == 0) moveDirection *= -1; Вставьте это в метод doMove() и вы сможете обрабатывать все события столкновений. Потанцуем? Все примеры роботов рассмотренных нами используют одну и ту же технику, чтобы двигатся вокруг своих врагов, с некоторыми незначительными вариациями. Вы можете сравнить их с любыми стандартными роботами. Кружимся Чтобы ездить вокруг врага используйте следующий код: public void doMove() { // изменяем направление движения, если произошла остановка if (getVelocity() == 0) moveDirection *= -1; // заворачиваем вокруг врага setTurnRight(enemy.getBearing() + 90); setAhead(1000 * moveDirection); } Робот Circler кружит вокруг врага используя вышеприведенный код. Он похож на акулу, которая плавает вокруг своей добычи. Стрейф Но есть одна проблема, которую вы возможно заметили. Circler - это легкая добыча для предупредительного огня, потому что его движения очень... предсказуемы. Поставьте против него PredictiveShooter и вы увидите, насколько быстро Circler потерпит поражение. Чтобы уклонятся от пуль более еффективно, вы должны стрейфится. Неплохой способ для реализации стрейфа - это переключение направления после какого-то определенного числа "тактов". Вот так: public void doMove() { // всегда принимать боевую позицию относительно врага setTurnRight(enemy.getBearing() + 90); // стрейфимся изменением направления каждые 20 тактов if (getTime() % 20 == 0) { moveDirection *= -1; setAhead(150 * moveDirection); } } Как это ни странно, MyFirstRobot делает нечто похожее и поэтому в него крайне сложно попасть. Strafer двигается вперед и назад, используя вышеприведенный код. Заметьте, насколько лучше он стал уклонятся от пуль. Наступление Наверно вы заметили... Да конечно, новую проблему. И Circler и Strafer могут легко попасть в углы и кончить тем, что просто будут бится об стены. Дополнительная проблема заключается в том, что если враг окажется достаточно далеко, наш робот будет много стрелять и мало попадать. Чтобы приблизить своего робота ближе к врагу, просто измените код "боевой стойки" чтобы заставить его слегка развернуться. Вот так: setTurnRight(normalizeBearing(enemy.getBearing() + 90 - (15 * moveDirection))); Spiraler это вариация на тему "Circler", которая использует этот код для выписывания спиралей вокруг врага. StrafeCloser это вариант Strafer который использует вышеприведенный код для стрейфа и приближения. Он также достаточно легко уклоняется от пуль. Заметьте, ни один из этих роботов не задерживается в углах надолго. Избегаем стены. Проблема всех роботов в том, что они много ударяются о стены. А ведь это забирает вашу енергию. Разумнее будет остановится перед препятсвием. Но как это сделать? Создание собственного события. Первая вещь, которую вы должны решить - это насколько близко мы позволим нашему роботу приблизится к стене. public class WallAvoider extends AdvancedRobot { ... private int wallMargin = 60; Далее, мы добавляем собственное событие. Мы будем его вызвать тогда, когда выполнится определенное условие. // Не приближаться слишком близко к стенам addCustomEvent(new Condition("too_close_to_walls") { public boolean test() { return ( // мы приблизились слишком близко к левой стене (getX() <= wallMargin || // или мы приблизились слишком близко к правой стене getX() >= getBattleFieldWidth() - wallMargin || // или мы приблизились слишком близко к нижней стене getY() <= wallMargin || // или мы приблизились слишком близко к верхней стене getY() >= getBattleFieldHeight() - wallMargin) ); } }); Заметьте, что мы создали анонимный внутренний класс этим вызовом. (Вы, парни, будете делать много подобных вещей когда мы начнем изучать GUI). Нам необходимо переписать метод text() чтобы возвратить логическое значнеие, когда случится созданное нами событие. Обработка созданного события. Следующее, что нам необходходимо - обработать наше событие. Это можно сделать так: public void onCustomEvent(CustomEvent e) { if (e.getCondition().getName().equals("too_close_to_walls")) { // переключить направление и уехать подальше moveDirection *= -1; setAhead(10000 * moveDirection); } } Возникла проблема - событие может вызыватся очень часто, так что мы будем постоянно менять направление движения и никогда не выберемся оттуда. JiggleOfDeath демонстрирует недостаток этого решения. Посмотрите на его битву с Walls и вы увидите, как наш робот ляжет. Чтобы избежать этого "танца смерти" у нас должна быть переменная, которая укажет на то, что сейчас мы обрабатываем событие. Мы можем обьявить ее примерно так: public class WallAvoider extends AdvancedRobot { ... private int tooCloseToWall = 0; обрабатываем событие немного умнее: public void onCustomEvent(CustomEvent e) { if (e.getCondition().getName().equals("too_close_to_walls")) { if (tooCloseToWall <= 0) { // если мы уже сталкивались со стенами, то сейчас нам нужно... tooCloseToWall += wallMargin; setMaxVelocity(0); // остановиться!!! } } } Использование двух режимов Осталось только две нерешенные проблемы. Во первых, у нас есть метод doMove(), где мы размеращали весь код, относящийся к движению. Когда мы пытаемся отъехать от стенки, нежелательно вызывать этот медот, чтобы не опять не вызвать "танец смерти". Во вторых, в конечном итоге, мы хотим передать управление назад, так что нужно обьявить переменную tooCloseToWall, с помощью которой мы это и сделаем. Мы можем решить обе проблемы, изменив doMove() следующим образом: public void doMove() { // всегда вставать в стойку против врага, развернувшись относительно него setTurnRight(enemy.getBearing() + 90 - (10 * moveDirection)); // если мы слишком близко от стены, то мы уезжаем отсюда if (tooCloseToWall > 0) tooCloseToWall--; // нормальное движение: переключаем направление если мы остановились if (getVelocity() == 0) { moveDirection *= -1; setAhead(10000 * moveDirection); } } WallAvoider использует приведенный выше код, чтобы избежать столкновения со стенами. Посмотрите на его битву с Walls и заметьте, как он плавно проходит мимо стен, но никогда не ударяется об них. Многорежимный бот Кроме выбранных вами цветов, наиболее уникальная часть робота - это алгоритм его движения. Но следует помнить, что разные ситуации требуют смену тактики. Используя приведенный выше пример, вы можете запрограммировать своего робота так, чтобы он изменял свой "режим" исходя из определенных критериев. Взяв упражнение из лабы 5 как точку отправления, я могу изменить робота методом, похожим на этот: public void onRobotDeath(RobotDeathEvent e) { ... if (getOthers() > 10) { // большая группа врагов требует плавающего движения tank = new CirclingTank(); } else if (getOthers() > 1) { // увороты - это лучшая тактика при небольшом количестве врагов tank = new DodgingTank(); } else if (getOthers() == 1) { // если остался только один бот, начинаем охоту за ним tank = new SeekAndDestroy(); } ... } Детали опущены ( как всегда ), как упражнение для студентов. ------------------------------------------------------------------------------- http://translated.by/you/robocode-lessons/into-ru/trans/ Original (English): Robocode Lessons (http://www.codepoet.org/~markw/weber/java/robocode/) Translation: © PersonaPrima, BAZIL, RANUX, alien713cea, teo-bon, Cyxapb, Passerby, vovka, ilyash11, Frankey, denton, aifgi, Triamor, gitstereophonic, general-manjago, max00007, alebezh. translated.by crowd