感知
雷达持续扫描,取得敌人的位置与状态。
Lecture 02 · Events, Scanning, and Firepower
学习 Event(事件)与 Event Handler(事件处理器),让坦克在扫描到敌人时自动判断距离并开火。
上一课我们让坦克动起来;这一课开始让它“看见”战场。编程里的聪明,通常来自感知、判断与行动的连接。
雷达持续扫描,取得敌人的位置与状态。
用 Variable(变量)与 If-Else(条件判断)决定火力。
在合适时机调用 fire(...),让策略变成战场行为。
知道 Event(事件)是“发生了某件事”,例如扫描到敌人或被子弹击中。
从 ScannedBotEvent 取得敌人的坐标、能量、方向与速度。
扫描后转向敌人,并用 If-Else(条件判断)按距离调整 firepower(火力)。
复习第一课:启动、移动、扫描与保存代码。
讲解 Event(事件)与扫描资料。
教师示范:瞄准、距离火力与能量管理。
学生实作、测试命中率、Debug 与 Git 保存。
事件可以想象成战场发来的通知:当雷达扫到敌人时,Robocode 会呼叫
onScannedBot(...)。
雷达像眼睛,事件像“我看到了”的通知,事件处理器就是“看到之后要做什么”。
@Override
public void onScannedBot(ScannedBotEvent e) {
double enemyX = e.getX();
double enemyY = e.getY();
double enemyEnergy = e.getEnergy();
fire(1);
}
getX() 与 getY():敌人的坐标。getEnergy():敌人当前能量。getDirection():敌人移动方向。getSpeed():敌人速度。上一课的 turnGunLeft(360) 不只是转炮塔,它也让坦克“扫描”了一圈:
onScannedBot(...)。如果坦克“看不到”敌人,多半是雷达没有持续转动。
在写瞄准与火力策略前,先补一点 Java 基础:程序里的资料都存放在变量 (variable) 里,而每个变量都要先说明它存放哪一种类型 (type) 的资料。Java 一共有 8 种基本类型 (primitive types):
| 类型 | 存放的资料 | 例子 |
|---|---|---|
boolean |
真或假 (true / false) | boolean moving = true; |
int |
整数(最常用的整数类型) | int round = 5; |
double |
小数(本课程最常用) | double angle = 90.5; |
char |
单个字符 | char grade = 'A'; |
long |
很大的整数 | long count = 9_000_000L; |
byte / short / float |
省内存的数字类型,本课程较少用到 | float f = 1.5f; |
本课程最常用三种:double(坐标、角度、距离、火力都是小数)、int(计数、回合数)、boolean(条件判断)。其余先知道存在即可。
从 Java 10 起,局部变量 (local variable) 可以写
var,由编译器根据右边的值自动推断类型,少写一次类型名。
var distance = distanceTo(e.getX(), e.getY()); // 推断为 double
var name = getName(); // 推断为 String
double firepower = 2; // 也可以照旧明确写出类型
var
只能用在有初始值的局部变量;方法参数、字段或没赋值的声明都不行。
var x = 1; 之后 x 永远是
int,不能再放小数。
var 让代码更简洁;初学阶段把
double / int 明确写出来也很好,更容易看懂。
变量像一个贴了标签的盒子。我们把计算结果放进去,后面就能用清楚的名字表达策略。
double distance = distanceTo(e.getX(), e.getY());
double firepower = 1;
double:可以存放小数的数字类型。distanceTo(x, y):Robocode
提供的助手方法,直接算出我方到某个坐标的距离,不用自己写数学公式。
firepower:这一发子弹要消耗多少能量。有了变量,就能用运算符 (operator) 对里面的数字做计算。最常用的是五个算术运算符:
| 运算符 | 意思 | 例子(int a = 7, b = 2;) |
|---|---|---|
+ |
加 | a + b → 9 |
- |
减 | a - b → 5 |
* |
乘 | a * b → 14 |
/ |
除 | a / b → 3(整数相除会丢掉小数!) |
% |
取余数 (modulo) | a % b → 1(7 除以 2 余 1) |
关键陷阱:两个整数 (int) 相除,结果也是整数,小数会被直接丢掉 (7 / 2得 3,不是 3.5)。本课程的坐标、距离、角度多是 double(小数),所以
distanceTo(...) / 2 这类计算能正常得到小数。
把运算符用在战场数据上,就能让火力随距离自动变化,而不是写死一个数字。
double distance = distanceTo(e.getX(), e.getY());
// 距离越近,火力越大;除法让远近自动换算成火力
double firepower = 360 / distance;
// 把火力限制在合法范围 0.1 ~ 3
firepower = Math.max(0.1, Math.min(3, firepower));
fire(firepower);
360 / distance:距离 120 时约 3,距离 600 时约 0.6,自动“近强远弱”。
Math.min(3, ...) 与
Math.max(0.1, ...):把结果夹在合法范围内,避免超过 3
或低于 0.1。
下一步要用条件判断分情况处理,先认识用来“比较”的运算符。
条件判断 (If-Else) 需要先问一个“是不是”的问题,答案是 true 或
false。这就要用比较运算符:
| 运算符 | 意思 | 例子 |
|---|---|---|
< / > |
小于 / 大于 | distance < 200 |
<= / >= |
小于等于 / 大于等于 | energy >= 10 |
== |
等于(判断是否相等) | round == 1 |
!= |
不等于 | name != null |
最常见的初学者错误:判断相等要用两个等号 ==;一个等号
= 是赋值(把右边的值放进变量),两者意义完全不同。
double distance = distanceTo(e.getX(), e.getY());
double firepower;
if (distance < 200) {
firepower = 3;
} else if (distance < 500) {
firepower = 2;
} else {
firepower = 1;
}
fire(firepower);
近距离命中机会较高,可以用较大火力;远距离命中风险较高,应保留能量。
开火不是免费的。火力 (firepower) 的范围是 0.1 ~ 3,它同时决定伤害、速度与代价:
| 项目 | 公式 / 数值 | 说明 |
|---|---|---|
| 消耗能量 | = 火力 | 开火立即扣除等于火力的能量 |
| 子弹伤害 | = 4 × 火力(火力 > 1 时再 + 2 ×(火力 − 1)) | 火力越大,伤害越高 |
| 命中返还能量 | = 3 × 火力 | 打中对手会回能量,鼓励命中 |
| 子弹速度 | = 20 − 3 × 火力 | 火力越小,子弹飞得越快 |
| 炮管热量 | = 1 + 火力 / 5 | 火力越大,越久不能再开火 |
所以“近距离强火力、远距离小火力”是有依据的:近距离命中率高,强火力的高伤害与回能量很划算;远距离难命中,小火力更省能量、子弹也更快。
参考: Robocode Physics
我们不必自己算三角函数。Robocode 提供了现成的助手方法
gunBearingTo(x, y),直接算出炮塔需要转多少度:
@Override
public void onScannedBot(ScannedBotEvent e) {
// 炮塔要转多少度才对准敌人(已自动归一到 -180 ~ 180)
double gunBearing = gunBearingTo(e.getX(), e.getY());
turnGunLeft(gunBearing);
fire(1);
}
gunBearingTo(x, y):
从炮塔当前方向看,要转多少度才对准目标。
turnGunLeft
接受负数时会自动向右转,所以一行就够了。
这正是模板里 TrackFire、RamFire 等示例 Bot 使用的写法。
double myEnergy = getEnergy();
double firepower = 1;
if (myEnergy < 20) {
firepower = 1;
} else if (distance < 200) {
firepower = 3;
}
getEnergy()
读取己方剩余能量。能量偏低时应保守开火,避免还没打到对手就先耗尽能量。
每次开火都会让炮管发热,热量大于 0 时无法再开火。每回合开局炮管热量为 3,需要时间冷却。
1 + 火力 / 5,火力越大冷却越久。getGunCoolingRate() 读取)。getGunHeat() == 0,才真正能射出子弹。if (getGunHeat() == 0) {
fire(firepower);
}
否则 fire(...) 会被忽略——这常是“坦克扫到了敌人却不开火”的真正原因。
@Override
public void run() {
while (isRunning()) {
forward(120);
turnGunLeft(360);
back(80);
turnGunRight(360);
}
}
@Override
public void onScannedBot(ScannedBotEvent e) {
// 1. 转动炮塔对准敌人
double gunBearing = gunBearingTo(e.getX(), e.getY());
turnGunLeft(gunBearing);
// 2. 按距离和自身能量决定火力
double distance = distanceTo(e.getX(), e.getY());
double firepower = getEnergy() < 20 ? 1 : (distance < 200 ? 3 : 1);
// 3. 炮管冷却好了才开火
if (getGunHeat() == 0) {
fire(firepower);
}
}
fire(1),观察能量消耗与命中情况。fire(3),观察火力变强后是否更快没能量。策略不是凭感觉决定,而是从观察结果中逐步修正。
加入敌方能量判断:如果 e.getEnergy() 很低,可以尝试更积极的火力。
import dev.robocode.tankroyale.botapi.events.*;。
firepower 默认值,再进入条件判断。
= 和比较混淆:判断小于要写
<,不是用赋值。
onScannedBot(...)。
fire() 会被忽略,先判断
getGunHeat()。
| 我改了什么 | 战场现象 | 下次假设 |
|---|---|---|
| 例:近距离 fire(3) | 例:命中提高但能量下降快 | 例:能量 < 20 时只开小火 |
实作时填写自己的观察;可与 commit message 一起保存。
onScannedBot(...)。
可选练习:模板仓库的
JammedGunBot 会扫到敌人却始终打不出子弹(故意写错),用 5 分钟集体找出它
onScannedBot
里的逻辑问题。
坦克没有开火时,先判断是没有扫描到敌人,还是事件处理器没有正确执行。
把问题缩小到 run() 的扫描动作,或
onScannedBot(...) 的开火逻辑。
排查顺序:先确认事件会发生,再确认事件发生后做了正确的事。
建议 commit message:Add scan aim and distance firepower。课后延伸:把火力规则写成中文说明,与代码一并 push(推送)到 GitHub。
趣味 Java 编程启蒙班
© 2026 by
Eng Zer Jun
is licensed under
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International