☰

教程 1:你在 Webots 中的第一个仿真(30 分钟)

本教程的第一个目标是让你熟悉用户界面以及 Webots 的基本概念。 你将创建自己的第一个仿真,其中包含一个简单的环境:一个带有地板和墙壁的竞技场、几个箱子、一个 e-puck 机器人,以及一个让机器人移动的控制器程序。

tutorial_e-puck.png

第一个教程结束时你应当看到的效果。

启动 Webots

如果尚未完成,请按照这些[安装说明(installing-webots.html)下载并安装 Webots。

实操 #1:双击 Webots 图标[启动 Webots(starting-webots.html)(或在终端中通过命令行启动它)。 如果你是在这台电脑上第一次运行 Webots,系统可能会提示你选择一个图形主题。 你也可能会被邀请体验 Webots 引导之旅,请直接关闭引导之旅即可。

如果你还没有看过引导之旅,请花点时间观看其中展示的演示。 它们充分展示了 Webots 的各种可能性。 引导之旅也可以从 Webots 的 Help(帮助)菜单中打开。

创建新世界

世界(World) 是一个文件,其中包含对象的位置、外观、对象之间的交互方式、天空的颜色等信息,以及重力、摩擦、对象质量等的定义。 它定义了仿真的初始状态。 其中的不同对象称为节点(Node),并在场景树(Scene Tree) 中以层次结构组织。 因此,一个节点可以包含子节点。 世界保存在扩展名为 .wbt 的文件中。 文件格式派生自 VRML97 语言,且人类可读。 世界文件必须直接存放在名为 worlds 的目录中。

实操 #2:点击 3D 视图中的 Pause(暂停)按钮 以暂停当前仿真(要了解各个按钮,请参见[用户界面说明(the-user-interface.html#simulation-menu))。 如果主工具栏上的虚拟时间计数器停止,则说明仿真已处于暂停状态。 通过 File / New / New Project Directory...(文件 / 新建 / 新建工程目录……)菜单项创建一个新项目,并按照提示操作:

  1. 将工程目录命名为 my_first_simulation,而不是建议的 my_project。
  2. 将世界文件命名为 my_first_simulation.wbt,而不是建议的 empty.wbt。
  3. 勾选所有复选框,包括默认未勾选的 "Add a rectangle arena"(添加矩形竞技场)。

Webots 会显示它刚刚创建的一系列目录和文件列表。 这对应于 [Webots 工程的标准文件层次结构(the-standard-file-hierarchy-of-a-project.html)。 点击 Finish(Windows、Linux)或 Done(macOS)按钮以关闭此窗口。

祝贺你,你刚刚创建了自己的第一个 Webots 世界! 3D 视图应该显示一个带棋盘格地板的正方形竞技场。 你可以[使用鼠标(the-3d-window.html#navigation-in-the-scene)在 3D 视图中移动视点:左键、右键和滚轮。

存放在世界文件中的 Webots 节点以树结构组织,称为场景树。 场景树可以在主窗口的两个子窗口中查看:[3D 视图(the-3d-window.html)(位于主窗口中央)是场景树的三维表示,而[场景树视图(the-scene-tree.html)(位于左侧)是场景树的层次化表示。 [场景树视图(the-scene-tree.html) 是修改节点和字段的地方。 它当前应列出以下节点:

每个节点都有一些可自定义的属性,称为字段(Field)。 让我们修改这些字段来改变矩形竞技场:

实操 #3:在场景树中双击 RectangleArena 节点。 这应该会展开该节点并显示它的字段。

  1. 选择 floorTileSize 字段,把它的值设置为 0.25 0.25,而不是 0.5 0.5。 你应该能在 3D 视图中立即看到效果。
  2. 选择 wallHeight 字段,把它的值改为 0.05,而不是 0.1。 竞技场的墙壁现在应该更低了。

在[场景树视图(the-scene-tree.html)中,如果字段的值与其默认值不同,则会以不同的颜色显示(取决于主题)。 现在,我们想添加一些对象:

实操 #4:在场景树中双击 RectangleArena 以关闭并选中它。 点击场景树顶部的 Add(添加)按钮 。 在打开的对话框中,选择 PROTO nodes (Webots Projects) / objects / factory / containers / WoodenBox (Solid)。 一个大箱子应该出现在竞技场中央。 在场景树中双击它以展开其字段。

  1. 将它的 size 改为 0.1 0.1 0.1,而不是 0.6 0.6 0.6。
  2. 将它的 translation 改为 0 0 0.05,而不是 0 0 0.3。 或者,你可以使用 3D 视图中出现的蓝色箭头来调整它的 translation.z 字段。
  3. 现在按住 shift 键点击箱子并拖动,把它移到竞技场的某个角落。
  4. 选中该箱子并按下 ctrl-C、ctrl-V(Windows、Linux)或 ⌘ command-C、⌘ command-V(macOS)来复制并粘贴它。 按住 shift 键点击并拖拽新箱子,把它移到另一个位置。 用同样的方式创建第三个箱子。
  5. 移动各个箱子,使没有任何箱子位于竞技场中心。 你也可以使用蓝色的旋转手柄绕垂直轴旋转箱子。 这也可以通过按住 shift 键并用鼠标右键点击拖拽来完成。 或者,你可以在场景树中修改 WoodenBox 节点的 rotation 字段的角度。
  6. 当你对结果满意后,使用保存按钮保存世界。

使用平移和旋转手柄移动对象的方法在[本节(the-3d-window.html#axis-aligned-handles)中有说明。

添加一个 e-puck 机器人

e-puck 是一个小型机器人,具有差速轮、10 个 [LED(https://cyberbotics.com/doc/reference/led) 以及多个传感器,包括 8 个 [DistanceSensors(https://cyberbotics.com/doc/reference/distancesensor) 和一个 [Camera(https://cyberbotics.com/doc/reference/camera)。 在本教程中,我们只关心使用它的轮子。 我们将在后续教程中学习如何使用其他功能。

现在,我们要向世界添加一个 e-puck 模型。 确保仿真处于暂停状态,且经过的虚拟时间为 0。 如果不是这样,请用 Reset(重置)按钮 重置仿真。

当修改 Webots 世界并打算保存时,至关重要的一点是:必须先暂停仿真并将其重新加载到初始状态,也就是说,主工具栏上的虚拟时间计数器应显示 0:00:00:000。 否则,每次保存时,每个三维对象的位置都会累积误差。 因此,对世界的任何修改都应按此顺序进行:暂停、重置、修改并保存仿真。

我们不需要从零开始创建 e-puck 机器人,只需要导入一个 E-puck 节点即可。 这个节点实际上是一个 [PROTO(https://cyberbotics.com/doc/reference/proto) 节点,就像我们之前介绍的 RectangleArena 或 WoodenBox。原型化允许你创建自定义对象并加以复用。

实操 #5:在场景树视图中选中最后一个节点 WoodenBox。 点击场景树视图顶部的 Add(添加)按钮 。 在对话框中,选择 PROTO nodes (Webots Projects) / robots / gctronic / e-puck / E-puck (Robot)。 一个 e-puck 机器人应该出现在竞技场中央。 用你移动箱子的同样方式移动并旋转这个机器人。 保存仿真并点击 Run real-time(实时运行)按钮 。

机器人应该会移动、闪烁 LED 并避开障碍物。 这是因为它带有一个具有该行为的默认控制器。 你可能已经注意到 3D 视图左上角出现了一个黑色小窗口。 它显示的是 e-puck 机器人的 [Camera(https://cyberbotics.com/doc/reference/camera) 拍摄的画面。 在机器人控制器显式启用摄像机之前,这个画面将保持黑色。 这个小的图像窗口可以通过拖动来[移动(the-3d-window.html#moving-and-resizing-overlays)。 也可以通过拖拽右下角来[调整大小(the-3d-window.html#moving-and-resizing-overlays)。 最后,点击右上角的 "x" 可以关闭它。 你可以从 Overlays(叠加层)菜单,在 Camera Devices(摄像机设备)子菜单中选中它来使其重新可见。 由于我们不需要它,你现在就可以把它关闭。

现在,在仿真运行期间,让我们来玩玩物理:

实操 #6:通过按下 alt + 左键点击 + 拖拽 对机器人[施加一个力(the-3d-window.html#applying-a-force-to-a-solid-object-with-physics)(某些 Mac 键盘上使用 ⌥ option 键)。 在 Linux 上,除了 alt + 左键点击 + 拖拽 之外,你还应按住 ctrl 键。 无法对 WoodenBox 节点施加力,因为默认情况下它们没有质量,被视为粘在地板上。 要在 WoodenBox 节点上启用物理,你应该把它们的 mass 字段设置为某个值(例如 0.2 kg)。 完成这一步之后,你也应该能对它们施加力了。

仿真可以被暂停 、逐步运行 、以实时 模式或以快速 模式运行。

现在我们要修改世界,减小物理仿真的步长:这会提高仿真的精度和稳定性(但会降低最大仿真速度)。

实操 #7:暂停仿真并回退。 在场景树视图中,展开 [WorldInfo(https://cyberbotics.com/doc/reference/worldinfo) 节点(第一个节点)。 将它的 basicTimeStep 字段设置为 16。 然后保存仿真。

创建新控制器

现在,我们将编写一个简单的控制器,只让机器人向前移动。

控制器 是一个定义机器人行为的程序。 Webots 控制器可以用以下编程语言编写:C、C++、Java、Python、MATLAB 等。 C、C++ 和 Java 控制器需要先编译,才能作为机器人控制器运行。 Python 和 MATLAB 控制器属于解释型语言,因此无需编译即可运行。 在本教程中,我们将以 C 作为参考语言,但所有代码片段同样提供 C++、Java、Python 和 MATLAB 版本。 如需使用其他编程语言设置控制器,请参考[编程语言章节(language-setup.html)。

Robot 节点的 controller 字段指定了当前与机器人关联的控制器。 注意,同一个控制器可以被多个机器人使用,但一个机器人一次只能使用一个控制器。 每个控制器都在一个独立的子进程中执行,该进程通常由 Webots 派生。 由于它们是独立的进程,控制器不共享同一地址空间,并且可以在不同的处理器核心上运行。

实操 #8:使用 File / New / New Robot Controller...(文件 / 新建 / 新建机器人控制器……)菜单项创建一个名为 epuck_go_forward 的新 C(或任何其他语言)控制器(对于 C++ 和 Java,请将其命名为 EPuckGoForward)。 这会在 my_first_simulation/controllers 中创建一个名为 epuck_go_forward(或 EPuckGoForward)的新目录。 选择提供在文本编辑器中打开源文件的选项。

新的源文件会显示在 Webots 文本编辑器窗口中。 该源文件无需任何修改即可编译(如果以 C、C++ 或 Java 编写),不过当前的代码并没有实际效果。 现在我们要把新的 epuck_go_forward(或 EPuckGoForward)控制器关联到 E-puck 节点。

实操 #9:在场景树视图中,选择 E-puck 节点的 controller 字段,然后使用场景树视图底部的字段编辑器:按下 Select...(选择……)按钮,然后在列表中选择 epuck_go_forward。 一旦控制器与机器人关联,保存世界。 通过插入一条 include 语句(#include <webots/motor.h>)、获取马达设备(WbDeviceTag motor = wb_robot_get_device("motor_name");),并应用一条马达命令(wb_motor_set_position(motor, 10);)来修改程序:

#include <webots/robot.h>

// Added a new include file
#include <webots/motor.h>

int main(int argc, char **argv) {
 wb_robot_init();

 // get the time step of the current world
 const int time_step = (int) wb_robot_get_basic_time_step();

 // get the motor devices
 WbDeviceTag left_motor = wb_robot_get_device("left wheel motor");
 WbDeviceTag right_motor = wb_robot_get_device("right wheel motor");
 // set the target position of the motors
 wb_motor_set_position(left_motor, 10.0);
 wb_motor_set_position(right_motor, 10.0);

 while (wb_robot_step(time_step) != -1);

 wb_robot_cleanup();

 return 0;
}

保存修改后的源代码(File / Save Text File),并进行编译(Build / Build)。 如有编译错误,请修正。 当 Webots 提出重置或重新加载世界时,选择 Reset 并运行仿真。

实操 #9:在场景树视图中,选择 E-puck 节点的 controller 字段,然后使用场景树视图底部的字段编辑器:按下 Select...(选择……)按钮,然后在列表中选择 EPuckGoForward。 一旦控制器与机器人关联,保存世界。 通过插入一条 include 语句(#include <webots/Motor.hpp>)、获取马达设备(Motor *leftMotor = robot->getMotor("left wheel motor");),并应用一条马达命令(leftMotor->setPosition(10.0);)来修改程序:

#include <webots/Robot.hpp>

// Added a new include file
#include <webots/Motor.hpp>

// All the webots classes are defined in the "webots" namespace
using namespace webots;

int main(int argc, char **argv) {
 Robot *robot = new Robot();

 // get the time step of the current world
 int timeStep = (int)robot->getBasicTimeStep();

 // get the motor devices
 Motor *leftMotor = robot->getMotor("left wheel motor");
 Motor *rightMotor = robot->getMotor("right wheel motor");
 // set the target position of the motors
 leftMotor->setPosition(10.0);
 rightMotor->setPosition(10.0);

 while (robot->step(timeStep) != -1);

 delete robot;

 return 0;
}

保存修改后的源代码(File / Save Text File),并进行编译(Build / Build)。 如有编译错误,请修正。 当 Webots 提出重置或重新加载世界时,选择 Reset 并运行仿真。

实操 #9:在场景树视图中,选择 E-puck 节点的 controller 字段,然后使用场景树视图底部的字段编辑器:按下 Select...(选择……)按钮,然后在列表中选择 epuck_go_forward。 一旦控制器与机器人关联,保存世界。 通过获取马达设备(leftMotor = robot.getDevice('left wheel motor')),并应用一条马达命令(leftMotor.setPosition(10.0))来修改程序:

from controller import Robot, Motor

# create the Robot instance.
robot = Robot()

# get the time step of the current world
timestep = int(robot.getBasicTimeStep())

# get the motor devices
leftMotor = robot.getDevice('left wheel motor')
rightMotor = robot.getDevice('right wheel motor')
# set the target position of the motors
leftMotor.setPosition(10.0)
rightMotor.setPosition(10.0)

while robot.step(timestep) != -1:
   pass

保存修改后的源代码(File / Save Text File),重置并运行仿真。

实操 #9:在场景树视图中,选择 E-puck 节点的 controller 字段,然后使用场景树视图底部的字段编辑器:按下 Select...(选择……)按钮,然后在列表中选择 EPuckGoForward。 一旦控制器与机器人关联,保存世界。 通过插入一条 import 语句(import com.cyberbotics.webots.controller.Motor;)、获取马达设备(Motor leftMotor = robot.getMotor("left wheel motor");),并应用一条马达命令(leftMotor.setPosition(10.0);)来修改程序:

import com.cyberbotics.webots.controller.Robot;

// Added a new include file
import com.cyberbotics.webots.controller.Motor;

public class EPuckGoForward {

 public static void main(String[] args) {

   Robot robot = new Robot();

   // get the time step of the current world.
   int timeStep = (int) robot.getBasicTimeStep();

   // get the motor devices
   Motor leftMotor = robot.getMotor("left wheel motor");
   Motor rightMotor = robot.getMotor("right wheel motor");
   // set the target position of the motors
   leftMotor.setPosition(10.0);
   rightMotor.setPosition(10.0);

   while (robot.step(timeStep) != -1);
 }
}

保存修改后的源代码(File / Save Text File),并进行编译(Build / Build)。 如有编译错误,请修正。 当 Webots 提出重置或重新加载世界时,选择 Reset 并运行仿真。

实操 #9:在场景树视图中,选择 E-puck 节点的 controller 字段,然后使用场景树视图底部的字段编辑器:按下 Select...(选择……)按钮,然后在列表中选择 epuck_go_forward。 一旦控制器与机器人关联,保存世界。 通过获取马达设备(left_motor = wb_robot_get_device('left wheel motor'))并应用一条马达命令(wb_motor_set_position(right_motor, 10.0))来修改程序:

function epuck_go_forward

% get the time step of the current world
TIME_STEP = wb_robot_get_basic_time_step();

% get the motor devices
left_motor = wb_robot_get_device('left wheel motor');
right_motor = wb_robot_get_device('right wheel motor');
% set the target position of the motors
wb_motor_set_position(left_motor, 10.0);
wb_motor_set_position(right_motor, 10.0);

while wb_robot_step(TIME_STEP) ~= -1
end

保存修改后的源代码(File / Save Text File),重置并运行仿真。

如果一切顺利,你的机器人应该会向前移动。 机器人将以最高速度移动一段时间,然后在轮子转过 10 弧度后停止。

在你的工程的 controllers 目录中,已经创建了一个包含 epuck_go_forward(或 EPuckGoForward)控制器的目录。 epuck_go_forward(或 EPuckGoForward)目录中包含一个 epuck_go_forward(或 EPuckGoForward)二进制文件,它是在控制器编译之后生成的(在 Windows 上,该文件带有 .exe 扩展名)。 控制器目录名应与二进制文件名一致。

扩展控制器以实现速度控制

机器人的轮子通常使用速度来控制,而不是像我们在上一个示例中那样使用位置。 要以速度控制轮子的马达,你需要把目标位置设置为无穷大,并设置期望的速度:

实操 #10:按下面所示修改控制器程序,重新编译并运行:

#include <webots/robot.h>

// Added a new include file
#include <webots/motor.h>

#define MAX_SPEED 6.28

int main(int argc, char **argv) {
  wb_robot_init();

  // get the time step of the current world
  const int time_step = (int) wb_robot_get_basic_time_step();

  // get a handler to the motors and set target position to infinity (speed control)
  WbDeviceTag left_motor = wb_robot_get_device("left wheel motor");
  WbDeviceTag right_motor = wb_robot_get_device("right wheel motor");
  wb_motor_set_position(left_motor, INFINITY);
  wb_motor_set_position(right_motor, INFINITY);

  // set up the motor speeds at 10% of the MAX_SPEED.
  wb_motor_set_velocity(left_motor, 0.1 * MAX_SPEED);
  wb_motor_set_velocity(right_motor, 0.1 * MAX_SPEED);

  while (wb_robot_step(time_step) != -1) {
  }

  wb_robot_cleanup();

  return 0;
}

实操 #10:按下面所示修改控制器程序,重新编译并运行:

#include <webots/Robot.hpp>

// Added a new include file
#include <webots/Motor.hpp>

#define MAX_SPEED 6.28

// All the webots classes are defined in the "webots" namespace
using namespace webots;

int main(int argc, char **argv) {
 Robot *robot = new Robot();

 // get the time step of the current world
 int timeStep = (int)robot->getBasicTimeStep();

 // get a handler to the motors and set target position to infinity (speed control)
 Motor *leftMotor = robot->getMotor("left wheel motor");
 Motor *rightMotor = robot->getMotor("right wheel motor");
 leftMotor->setPosition(INFINITY);
 rightMotor->setPosition(INFINITY);

 // set up the motor speeds at 10% of the MAX_SPEED.
 leftMotor->setVelocity(0.1 * MAX_SPEED);
 rightMotor->setVelocity(0.1 * MAX_SPEED);

 while (robot->step(timeStep) != -1);

 delete robot;

 return 0;
}

实操 #10:按下面所示修改控制器程序,重新编译并运行:

from controller import Robot, Motor

MAX_SPEED = 6.28

# create the Robot instance.
robot = Robot()

# get the time step of the current world
timestep = int(robot.getBasicTimeStep())

# get a handler to the motors and set target position to infinity (speed control)
leftMotor = robot.getDevice('left wheel motor')
rightMotor = robot.getDevice('right wheel motor')
leftMotor.setPosition(float('inf'))
rightMotor.setPosition(float('inf'))

# set up the motor speeds at 10% of the MAX_SPEED.
leftMotor.setVelocity(0.1 * MAX_SPEED)
rightMotor.setVelocity(0.1 * MAX_SPEED)

while robot.step(timestep) != -1:
   pass

实操 #10:按下面所示修改控制器程序,重新编译并运行:

import com.cyberbotics.webots.controller.Robot;

// Added a new include file
import com.cyberbotics.webots.controller.Motor;

public class EPuckGoForward {

 public static void main(String[] args) {

   double MAX_SPEED = 6.28;

   Robot robot = new Robot();

   // get the time step of the current world.
   int timeStep = (int)robot.getBasicTimeStep();

   // get a handler to the motors and set target position to infinity (speed control)
   Motor leftMotor = robot.getMotor("left wheel motor");
   Motor rightMotor = robot.getMotor("right wheel motor");
   leftMotor.setPosition(Double.POSITIVE_INFINITY);
   rightMotor.setPosition(Double.POSITIVE_INFINITY);

   // set up the motor speeds at 10% of the MAX_SPEED.
   leftMotor.setVelocity(0.1 * MAX_SPEED);
   rightMotor.setVelocity(0.1 * MAX_SPEED);

   while (robot.step(timeStep) != -1);
 }
}

实操 #10:按下面所示修改控制器程序,重新编译并运行:

function epuck_go_forward

MAX_SPEED = 6.28;

% get the time step of the current world
TIME_STEP = wb_robot_get_basic_time_step();

% get a handler to the motors and set target position to infinity (speed control)
left_motor = wb_robot_get_device('left wheel motor');
right_motor = wb_robot_get_device('right wheel motor');
wb_motor_set_position(left_motor, inf);
wb_motor_set_position(right_motor, inf);

% set up the motor speeds at 10% of the MAX_SPEED.
wb_motor_set_velocity(left_motor, 0.1 * MAX_SPEED);
wb_motor_set_velocity(right_motor, 0.1 * MAX_SPEED);

while wb_robot_step(TIME_STEP) ~= -1
end

现在机器人将会移动(轮子将以每秒 0.2 弧度的速度旋转)并且永不停止。 如果没有任何反应,别忘了通过选择 Build / Build 菜单项或点击代码区域上方的齿轮图标 来编译你的代码。 编译错误会以红色显示在控制台中。 如果有错误,请修正它们并重新尝试编译。 然后,重新加载世界。

结论

我们希望你喜欢上了创建自己的第一个 Webots 仿真。 你已经能够搭建一个世界、添加一个机器人并对其进行编程。 重要的是,你学到了下面总结的基本概念: