
《星际争霸:巢穴之战》 。 这个游戏对我来说意义非凡! 我想对你们中的许多人来说。 如此之多,我想知道是否应该在Wikipedia上提供其页面的链接。
一次霍尔特给我发了PM并提议学习Rust 。 像任何普通人一样,我们决定从 你好世界 为Windows编写一个动态库,该库可以加载到StarCraft的地址空间中并管理单元。
下面的文章将描述寻找解决方案以及使用允许您学习Rust及其生态系统新事物的技术的过程。 可能还会启发您使用自己喜欢的语言(无论是C,C ++,Ruby,Python等)来实现机器人。
阅读本文时,绝对值得听听韩国的赞美诗:
布瓦皮
这场比赛已经有20年历史了。 而且仍然很受欢迎 ; 甚至在2017年 ,冠军争夺战在美国也引起了人们的注意, 在那里大师级的Jaedong与Bisu的战斗发生了。 除了人类玩家之外, 毫无灵魂的机器还参加了SC战斗! 由于BWAPI ,这是可能的。 更有用的链接 。
十多年来,围绕这个游戏的机器人开发者社区已经出现。 热心者创造机器人并参加各种锦标赛。 他们中的许多人都学习AI和机器学习。 大学使用BWAPI训练学生。 甚至还有一个抽搐频道播放此类比赛。
因此,几年前,一个粉丝团队扭转了星际争霸的后端,并开发了C ++ API,该API可让您创建机器人,在游戏过程中进行注入并控制可悲的人类。
经常发生 盖房子,有必要开采矿石,锻造工具 创建一个机器人,您需要实现一个API。 Rust必须提供什么?
联邦调查局
使用Rust中的其他语言非常简单。 为此有一个FFI 。 让我从文档中摘录一下 。
想象一下,我们有一个快照库,其中包含一个头文件snappy-ch ,其中包含函数声明。
让我们使用cargo创建一个项目。
$ cargo new --bin snappy Created binary (application) `snappy` project $ cd snappy snappy$ tree . ├── Cargo.toml └── src └── main.rs 1 directory, 2 files
Cargo为该项目创建了标准文件结构。
在Cargo.toml
我们指定对libc的依赖关系:
[dependencies] libc = "0.2"
src/main.rs
文件将如下所示:
extern crate libc;
让我们构建并运行项目:
snappy$ cargo build ... snappy$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/snappy` max compressed length of a 100 byte buffer: 148
您只能叫cargo run
,这要在运行前叫cargo build
。 另一种选择是构建项目并直接调用二进制文件:
snappy$ ./target/debug/snappy max compressed length of a 100 byte buffer: 148
如果已安装snappy库,则将编译代码(对于Ubuntu,应安装libsnappy-dev软件包)。
snappy$ ldd target/debug/snappy ... libsnappy.so.1 => /usr/lib/x86_64-linux-gnu/libsnappy.so.1 (0x00007f8de07cf000)
如您所见,我们的二进制文件链接到libsnappy共享库。 在我们的代码中对snappy_max_compressed_length
的调用是从该库进行的函数调用。
锈结合
如果我们能够自动生成FFI,那将是很好的。 幸运的是,Rust瘾君子的工具箱中有一个名为rust-bindgen的实用程序。 它能够生成与C(和某些C ++)库的FFI绑定。
安装方式:
$ cargo install bindgen
锈菌结合素看起来像什么? 我们使用C / C ++头文件,将bindgen实用程序指向它们,然后将获得的输出生成带有适当声明的Rust代码,以使我们使用C结构和函数。 这是bindgen
为bindgen
生成的:
$ bindgen /usr/include/snappy-ch | grep -C 1 snappy_max_compressed_length extern "C" { pub fn snappy_max_compressed_length(source_length: usize) -> usize; }
事实证明,bindgen无法应付BWAPI标头,生成大量不可用的代码(由于虚拟成员函数,公共API中的std ::字符串等)。 事实是BWAPI用C ++编写。 即使在C ++项目中,C ++通常也很难使用。 库组装完成后,最好使用相同的链接器(相同版本)链接它,头文件应使用相同的编译器(相同版本)进行解析。 所有这些因素都可能影响结果。 例如, 重整 ,仍然无法在GNU GCC中没有错误地实现 。 这些因素非常重要,甚至gtest也无法克服。 并且在文档中说:您最好使用相同的编译器和相同的链接器将gtest构建为项目的一部分。
布瓦比克
C是软件工程的通用语。 如果rust-bindgen适用于C语言,为什么不为C实现BWAPI,然后使用其API? 好主意!
是的,这是一个好主意,直到您深入研究BWAPI的内部并看到应该实现的类和方法的数量。 特别是所有这些我们没有时间进行的内存布局,asm代码,内存修补和其他“恐怖”。 有必要完全使用现有解决方案。
但是,我们需要以某种方式击败混乱,C ++代码,继承和虚拟成员函数。
在C ++中,有两个功能强大的工具extern "C"
来解决问题: 不透明的指针和extern "C"
。
extern "C" {}
允许C ++代码在C语言下“屏蔽”自身。它可以生成函数名称而无需处理。
不透明的指针使我们可以清除类型并创建指向“某种类型”的指针,而无需提供其实现。 由于这只是某种类型的声明,因此无法通过值使用此类型,只能通过指针使用它。
假设我们有以下C ++代码:
namespace cpp { struct Foo { int bar; virtual int get_bar() { return this->bar; } }; }
我们可以将其转换为C标头:
extern "C" { typedef struct Foo_ Foo; // Opaque pointer to Foo // call cpp::Foo::get_bar int Foo_get_bar(Foo* self); }
这是C ++部分,它将成为C标头和C ++实现之间的链接:
int Foo_get_bar(Foo* self) {
并非所有的类方法都必须以这种方式处理。 在BWAPI中,可以使用这些结构的字段来实现自己的类,例如typedef struct Position { int x; int y; } Position;
typedef struct Position { int x; int y; } Position;
以及Position::get_distance
类的方法。
有些课程我必须以特殊的方式对待。 例如,AIModule应该是指向具有特定虚拟成员函数集的C ++类的指针。 不过,这是标题和实现 。
因此,经过几个月的努力, 554种方法和十几个类,跨平台库BWAPI-C诞生了,它使您可以用C创建机器人。 副产品是交叉编译的可能性以及以支持FFI和cdecl调用约定的任何其他语言实现API的能力。
如果要编写库,请使用C编写其API。
BWAPI-C的最重要功能是与其他编程语言的尽可能广泛的集成。 Python
, Ruby
, Rust
, PHP
, Java
和许多其他功能都可以使用C,因此,如果您解决了一些难题并实现了自己的包装器,则还可以在它们的帮助下编写一个机器人。
用C编写机器人
本部分描述了Starcraft模块内部组织的一般原理。
机器人有2种类型:模块和客户端。 让我们看一个编写模块的例子。
该模块是一个动态库。 加载动态库的一般原理可以在这里查看。 该模块应导出2个函数: newAIModule
和gameInit
。
gameInit
很容易。 调用此函数可将指针传递给当前游戏。 该指针非常重要,因为在代码的某些部分中使用了BWAPI的通配符,其中包含全局静态变量。 让我们描述gameInit
:
DLLEXPORT void gameInit(void* game) { BWAPIC_setGame(game); }
newAIModule
有点复杂。 它应该返回指向C ++类的指针,该类具有一个虚拟方法表,其名称类似于onXXXXX ,在某些游戏事件中会被调用。 让我们声明模块的结构:
typedef struct ExampleAIModule { const AIModule_vtable* vtable_; const char* name; } ExampleAIModule;
第一个字段应该是指向方法表的指针(这是一种魔术)。 这是newAIModule
函数:
DLLEXPORT void* newAIModule() { ExampleAIModule* const module = (ExampleAIModule*) malloc( sizeof(ExampleAIModule) ); module->name = "ExampleAIModule"; module->vtable_ = &module_vtable; return createAIModuleWrapper( (AIModule*) module ); }
createAIModuleWrapper
是另一个魔术,可以将C指针转换为指向C ++类的指针, 虚方法 成员函数。
module_vtable
是方法表上的静态变量,方法值填充有指向全局函数的指针:
static AIModule_vtable module_vtable = { onStart, onEnd, onFrame, onSendText, onReceiveText, onPlayerLeft, onNukeDetect, onUnitDiscover, onUnitEvade, onUnitShow, onUnitHide, onUnitCreate, onUnitDestroy, onUnitMorph, onUnitRenegade, onSaveGame, onUnitComplete }; void onEnd(AIModule* module, bool isWinner) { } void onFrame(AIModule* module) {} void onSendText(AIModule* module, const char* text) {} void onReceiveText(AIModule* module, Player* player, const char* text) {} void onPlayerLeft(AIModule* module, Player* player) {} void onNukeDetect(AIModule* module, Position target) {} void onUnitDiscover(AIModule* module, Unit* unit) {} void onUnitEvade(AIModule* module, Unit* unit) {} void onUnitShow(AIModule* module, Unit* unit) {} void onUnitHide(AIModule* module, Unit* unit) {} void onUnitCreate(AIModule* module, Unit* unit) {} void onUnitDestroy(AIModule* module, Unit* unit) {} void onUnitMorph(AIModule* module, Unit* unit) {} void onUnitRenegade(AIModule* module, Unit* unit) {} void onSaveGame(AIModule* module, const char* gameName) {} void onUnitComplete(AIModule* module, Unit* unit) {}
如果查看函数的名称及其签名,则很清楚在什么条件下以及需要使用哪些参数调用它们。 例如,我将所有功能都清空了,除了
void onStart(AIModule* module) { ExampleAIModule* self = (ExampleAIModule*) module; Game* game = BWAPIC_getGame(); Game_sendText(game, "Hello from bwapi-c!"); Game_sendText(game, "My name is %s", self->name); }
游戏运行时调用此函数。 该参数是指向当前模块的指针。 BWAPIC_getGame
返回指向游戏的全局指针,我们使用对BWAPIC_setGame
的调用来设置该BWAPIC_setGame
。 因此,让我们展示一个交叉编译模块的工作示例:
bwapi-c/example$ tree . ├── BWAPIC.dll └── Dll.c 0 directories, 2 files bwapi-c/example$ i686-w64-mingw32-gcc -mabi=ms -shared -o Dll.dll Dll.c -I../include -L. -lBWAPIC bwapi-c/example$ cp Dll.dll ~/Starcraft/bwapi-data/ bwapi-c/example$ cd ~/Starcraft/bwapi-data/ Starcraft$ wine bwheadless.exe -e StarCraft.exe -l bwapi-data/BWAPI.dll --headful ... ... ...
按下按钮并运行游戏。 可以在BWAPI网站和BWAPI-C中找到有关编译和执行的更多信息。
模块的结果:

您可以在bwapi-c / example / Dll.c中找到一个更复杂的模块示例,其中显示了如何使用迭代器,单元管理,矿物搜索和统计信息输出。
bwapi系统
在Rust生态系统中,有一种命名程序包的方式可以链接到本机库。 任何软件包foo-sys都执行两个重要功能:
- 与本地库libfoo的链接;
- 提供libfoo库中函数的声明。 但是只有声明! * -sys包装箱中未提供高级抽象。
为了使* -sys软件包能够成功链接,您必须告诉cargo 搜索本机库和/或从源代码构建库。
为了使* -sys包提供声明,您需要自己编写它们或使用bindgen生成它们。 再次bindgen。 尝试第二个=)
绑定的生成非常简单:
bindgen BWAPI.h -o lib.rs \ --opaque-type ".+_" \ --blacklist-type "std.*|__.+|.+_$|Game_v(Send|Print|Draw).*|va_list|.+_t$" \ --no-layout-tests \ --no-derive-debug \ --raw-line "#![allow(improper_ctypes, non_snake_case)]" \ -- -I../submodules/bwapi-c/include sed -i -r -- 's/.+\s+(.+)_;/pub struct \1;/' lib.rs
BWAPI.h
是具有BWAPI-C中所有C头的文件。
例如,bindgen已经为上面的函数生成了这样的声明:
extern "C" {
有两种策略:将生成的代码存储在存储库中,以及在构建过程中即时生成代码。 两种方法都有其优点和缺点 。
很高兴认识您bwapi-sys ; 朝着我们的目标又迈出了一步。
您还记得我之前谈论过跨平台吗? nlinker加入了该项目并实施了一项狡猾的策略。 如果目标主机是Windows,则从GitHub下载已经组装好的BWAPIC。 对于其余的目标,我们从OpenBW的来源中收集BWAPI-C(稍后再介绍)。
布瓦皮尔
现在我们有了绑定,我们可以定义高级抽象了。 我们有两种类型可以使用:纯值和不透明指针。
一切都很简单,只有纯价值。 让我们以颜色为例。 我们需要使Rust代码易于使用,以便以方便自然的方式使用颜色:
game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red); ^^^
因此,为了方便使用,有必要使用C ++ 常量定义枚举,还应使用Rust惯用的枚举,并定义使用std :: convert :: From在bwapi_sys :: Color中对其进行转换的方法:
为了您的方便,您可以使用枚举基元派生的板条箱。
使用不透明指针也很容易。 让我们使用Newtype模式:
pub struct Player(*mut sys::Player);
这意味着Player是一种带有私有字段的结构-C语言的原始不透明指针。这是定义Player :: color的方式:
impl Player {
现在我们可以用Rust编写我们的第一个机器人了!
在Rust中创建一个机器人
作为概念验证,该机器人将类似于一个知名的countru:整个任务是雇用工人并收集矿物质。


让我们从所需的功能gameInit
和newAIModule
:
#[no_mangle] pub unsafe extern "C" fn gameInit(game: *mut void) { bwapi_sys::BWAPIC_setGame(game as *mut bwapi_sys::Game); } #[no_mangle] pub unsafe extern "C" fn newAIModule() -> *mut void { let module = ExampleAIModule { name: String::from("ExampleAIModule") }; let result = wrap_handler(Box::new(module)); result }
#[no_mangle]
与C ++中的extern "C"
执行相同的功能。 在wrap_handler
内部, wrap_handler
所有魔术,其中包括虚拟函数表的替换和“掩盖” C ++类。
模块结构的定义比C语言更简单,更奇特:
struct ExampleAIModule { name: String, }
让我们添加一些方法来呈现统计信息和发出订单:
impl ExampleAIModule { fn draw_stat(&mut self) { let game = Game::get(); let message = format!("Frame {}", game.frame_count()); game.draw_text(CoordinateType::Screen, (10, 10), &message); } fn give_orders(&mut self) { let player = Game::get().self_player(); for unit in player.units() { match unit.get_type() { UnitType::Terran_SCV | UnitType::Zerg_Drone | UnitType::Protoss_Probe => { if !unit.is_idle() { continue; } if unit.is_carrying_gas() || unit.is_carrying_minerals() { unit.return_cargo(false); continue; } if let Some(mineral) = Game::get() .minerals() .min_by_key(|m| unit.distance_to(m)) {
要将ExampleAIModule类型转换为实际模块,您需要使其对onXXXX事件做出响应。 为此,您需要实现EventHandler类型,该类型类似于C语言中的AIModule_vtable虚拟表:
impl EventHandler for ExampleAIModule { fn on_start(&mut self) { Game::get().send_text(&format!("Hello from Rust! My name is {}", self.name)); } fn on_end(&mut self, _is_winner: bool) {} fn on_frame(&mut self) { self.draw_stat(); self.give_orders(); } fn on_send_text(&mut self, _text: &str) {} fn on_receive_text(&mut self, _player: &mut Player, _text: &str) {} fn on_player_left(&mut self, _player: &mut Player) {} fn on_nuke_detect(&mut self, _target: Position) {} fn on_unit_discover(&mut self, _unit: &mut Unit) {} fn on_unit_evade(&mut self, _unit: &mut Unit) {} fn on_unit_show(&mut self, _unit: &mut Unit) {} fn on_unit_hide(&mut self, _unit: &mut Unit) {} fn on_unit_create(&mut self, _unit: &mut Unit) {} fn on_unit_destroy(&mut self, _unit: &mut Unit) {} fn on_unit_morph(&mut self, _unit: &mut Unit) {} fn on_unit_renegade(&mut self, _unit: &mut Unit) {} fn on_save_game(&mut self, _game_name: &str) {} fn on_unit_complete(&mut self, _unit: &mut Unit) {} }
生成和运行模块就像使用C一样简单:
bwapi-rs$ cargo build --example dll --target=i686-pc-windows-gnu bwapi-rs$ cp ./target/i686-pc-windows-gnu/debug/examples/dll.dll ~/Starcraft/bwapi-data/Dll.dll bwapi-rs$ cd ~/Starcraft/bwapi-data/ Starcraft$ wine bwheadless.exe -e StarCraft.exe -l bwapi-data/BWAPI.dll --headful ... ... ...
和作品的视频:
Openbw
这些家伙走得更远。 他们决定编写SC的开源版本:BW! 他们很擅长。 他们的目标之一是实现HD图像,但SC:Remastered领先于他们=(目前,您可以使用其API编写机器人程序(是的,在C ++中也是如此),但是最令人惊奇的功能是能够查看直接在浏览器中回放。
结论
该实现存在一个未解决的问题:我们不控制引用的唯一性,因此在修改对象时, &mut
和&
到同一区域的存在将导致未定义的行为。 有点麻烦。 Halt 尝试实现惯用的绑定,但是他没有设法找到解决方案。 另外,如果您想完成此任务,则必须仔细“铲起” C ++ API并正确放置const
限定符。
我真的很享受这个项目的工作,观看了重播종일,并沉浸在氛围中。 该游戏在宇宙中产生了凹痕。 在SC:BW中,没有一款游戏能像现在这样流行,它对대한민국정치的影响是不可想象的。 韩国的职业玩家与黄金时段的韩国多拉姆广播一样受欢迎。 또한,프로프이수수수수수수수수
星际争霸万岁!
友情链接
非常感谢Steve Klabnik帮助我审阅了这篇文章。