Lecture 02 · Events, Scanning, and Firepower

坦克的感官

学习 Event(事件)与 Event Handler(事件处理器),让坦克在扫描到敌人时自动判断距离并开火。

Event(事件) ScannedBotEvent(扫描事件) Variable(变量) If-Else(条件判断)

从“会动”到“会反应”

上一课我们让坦克动起来;这一课开始让它“看见”战场。编程里的聪明,通常来自感知、判断与行动的连接。

感知

雷达持续扫描,取得敌人的位置与状态。

判断

用 Variable(变量)与 If-Else(条件判断)决定火力。

行动

在合适时机调用 fire(...),让策略变成战场行为。

今天的学习目标

理解事件

知道 Event(事件)是“发生了某件事”,例如扫描到敌人或被子弹击中。

读取资料

ScannedBotEvent 取得敌人的坐标、能量、方向与速度。

瞄准与火力

扫描后转向敌人,并用 If-Else(条件判断)按距离调整 firepower(火力)。

2 小时课堂节奏

0:00 - 0:15

复习第一课:启动、移动、扫描与保存代码。

0:15 - 0:40

讲解 Event(事件)与扫描资料。

0:40 - 1:05

教师示范:瞄准、距离火力与能量管理。

1:05 - 2:00

学生实作、测试命中率、Debug 与 Git 保存。

什么是 Event(事件)?

事件可以想象成战场发来的通知:当雷达扫到敌人时,Robocode 会呼叫 onScannedBot(...)

  • Method(方法):一段可被呼叫的代码。
  • Event Handler(事件处理器):专门处理某类事件的方法。
  • Event Data(事件资料):事件发生时附带的坐标、能量、速度等数据。

课堂比喻

雷达像眼睛,事件像“我看到了”的通知,事件处理器就是“看到之后要做什么”。

扫描事件给我们什么资料?

@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) 不只是转炮塔,它也让坦克“扫描”了一圈:

  • 默认情况下,雷达 (Radar) 装在炮塔 (Gun) 上,炮塔装在车身 (Body) 上。
  • 所以转动炮塔时,雷达会跟着扫过战场。
  • 雷达一旦扫到敌人,游戏立即呼叫 onScannedBot(...)

如果坦克“看不到”敌人,多半是雷达没有持续转动。

雷达默认装在炮塔上,转动炮塔会带动雷达扫过敌人并触发 onScannedBot

Java 的基本类型 (Primitive Types)

在写瞄准与火力策略前,先补一点 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(条件判断)。其余先知道存在即可。

var:让 Java 自己推断类型

从 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 明确写出来也很好,更容易看懂。

Variable(变量):给资料取名字

变量像一个贴了标签的盒子。我们把计算结果放进去,后面就能用清楚的名字表达策略。

double distance = distanceTo(e.getX(), e.getY());
double firepower = 1;
  • double:可以存放小数的数字类型。
  • distanceTo(x, y):Robocode 提供的助手方法,直接算出我方到某个坐标的距离,不用自己写数学公式。
  • firepower:这一发子弹要消耗多少能量。

运算符 (Operators):让数字算出策略

有了变量,就能用运算符 (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) 需要先问一个“是不是”的问题,答案是 truefalse。这就要用比较运算符:

运算符 意思 例子
< / > 小于 / 大于 distance < 200
<= / >= 小于等于 / 大于等于 energy >= 10
== 等于(判断是否相等) round == 1
!= 不等于 name != null

最常见的初学者错误:判断相等要用两个等号 ==;一个等号 =赋值(把右边的值放进变量),两者意义完全不同。

If-Else(条件判断):让策略分情况

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 接受负数时会自动向右转,所以一行就够了。

这正是模板里 TrackFireRamFire 等示例 Bot 使用的写法。

Energy(能量):别在弱势时耗尽火力

double myEnergy = getEnergy();
double firepower = 1;

if (myEnergy < 20) {
    firepower = 1;
} else if (distance < 200) {
    firepower = 3;
}

getEnergy() 读取己方剩余能量。能量偏低时应保守开火,避免还没打到对手就先耗尽能量。

炮管热量 (Gun Heat):为什么 fire() 有时没反应?

每次开火都会让炮管发热,热量大于 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);
    }
}

教师示范:三组实验

  1. 固定 fire(1),观察能量消耗与命中情况。
  2. 改成 fire(3),观察火力变强后是否更快没能量。
  3. 使用距离判断,让坦克在近、中、远距离采用不同火力。

策略不是凭感觉决定,而是从观察结果中逐步修正。

学生练习:写出自己的火力规则

基础任务

  • 完成至少两个距离区间,例如小于 250、大于等于 250。
  • 每个区间使用不同 firepower(火力)。
  • 测试三场战斗,记录哪一场能量保留最好。

进阶挑战

加入敌方能量判断:如果 e.getEnergy() 很低,可以尝试更积极的火力。

常见错误

  • 忘记导入事件包:需要 import dev.robocode.tankroyale.botapi.events.*;
  • 变量还没赋值就使用:先给 firepower 默认值,再进入条件判断。
  • = 和比较混淆:判断小于要写 <,不是用赋值。
  • 雷达没有转动:如果不扫描,就不会触发 onScannedBot(...)
  • 火力过大:开火会消耗能量,不是越大越好。
  • 炮管还烫就开火:热量 > 0 时 fire() 会被忽略,先判断 getGunHeat()

策略日记(第一笔)

我改了什么 战场现象 下次假设
例:近距离 fire(3) 例:命中提高但能量下降快 例:能量 < 20 时只开小火

实作时填写自己的观察;可与 commit message 一起保存。

坦克诊疗室(15 分钟)

  1. 两人一组,交换阅读对方的 onScannedBot(...)
  2. 用 Debugging 记录表口头诊断:不开火、乱开火、能量掉太快。
  3. 每人只提一个可小步验证的修改建议。

可选练习:模板仓库的 JammedGunBot 会扫到敌人却始终打不出子弹(故意写错),用 5 分钟集体找出它 onScannedBot 里的逻辑问题。

Debugging(调试)练习

观察现象

坦克没有开火时,先判断是没有扫描到敌人,还是事件处理器没有正确执行。

定位代码

把问题缩小到 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