StarCraft: حرب الحضنة . كم يعني ذلك بالنسبة لي. ولعديد منكم. لدرجة أنني شككت في ما إذا كان يجب إعطاء رابط للويكي.
ذات مرة ، دقتني هالت في رسالة بريد إلكتروني شخصية وعرضت أن أتعلم الصدأ . قررنا ، مثل أي شخص عادي ، أن نبدأ مرحبا بالعالم كتابة مكتبة ديناميكية لـ Windows يمكن تحميلها في مساحة عنوان لعبة StarCraft وإدارة الوحدات.
ستصف المقالة عملية إيجاد الحلول ، باستخدام التقنيات ، والتقنيات التي ستسمح لك بتعلم أشياء جديدة في لغة Rust ونظامها البيئي أو أن تكون مصدر إلهام لتنفيذ روبوت في لغتك المفضلة ، سواء كانت C ، C ++ ، روبي ، بيثون ، إلخ.
هذه المقالة تستحق القراءة بالتأكيد تحت النشيد الكوري الجنوبي:
بوابي
هذه اللعبة بالفعل 20 سنة. ولا تزال تحظى بشعبية كبيرة ، حيث تجمع البطولات قاعات كاملة من الناس في الولايات المتحدة الأمريكية حتى في عام 2017 ، حيث وقعت معركة الأجداد Jaedong vs Bisu. بالإضافة إلى اللاعبين الأحياء ، تشارك السيارات الخالية من الروح في المعارك أيضًا! وهذا ممكن بفضل BWAPI . المزيد من الروابط المفيدة.
لأكثر من عقد من الزمان ، كان هناك مجتمع من مطوري برامج الروبوت حول هذه اللعبة. المتحمسين يكتبون البوتات ويشاركون في بطولات مختلفة. يدرس الكثير منهم الذكاء الاصطناعي والتعلم الآلي. تستخدم الجامعات BWAPI لتعليم طلابها. حتى أن هناك قناة نشل تبث الألعاب.
لذا ، قام فريق من المعجبين قبل بضع سنوات بعكس الأجزاء الداخلية من Starcraft وكتبوا واجهة برمجة تطبيقات C ++ تسمح لك بكتابة الروبوتات ، والاندماج في عملية اللعبة والسيطرة على الأشخاص الصغار المثير للشفقة.
كما يحدث غالبا من قبل لبناء منزل ، تحتاج إلى الحصول على خام ، وصياغة أدوات ... كتابة روبوت ، تحتاج إلى تنفيذ API. ماذا يمكن أن تقدم الصدأ؟
Ffi
التفاعل مع لغات أخرى من Rust بسيط للغاية. هناك FFI لهذا. اسمحوا لي أن أقدم مقتطفًا موجزًا من الوثائق .
افترض أن لدينا مكتبة سريعة تحتوي على ملف رأس snapp-ch الذي سنقوم بنسخ إعلانات الوظائف منه.
إنشاء مشروع باستخدام البضائع .
$ 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
ملف 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 تلقائيًا. لحسن الحظ ، هناك مثل هذه الأداة المساعدة في ترسانة rastomanov تسمى rust-bindgen . وهي قادرة على إنشاء روابط FFI لمكتبات C (وبعض C ++).
التثبيت:
$ cargo install bindgen
كيف يبدو استخدام الصدأ - bindgen ؟ نأخذ ملفات رأس C / C ++ ، ونضع أداة bindgen عليها ، ونحصل على كود Rust الذي تم إنشاؤه مع تعريفات الهياكل والوظائف الهيكلية. إليك ما يبدو عليه جيل FFI لـ snappy:
$ 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 ، مما يولد أطنانًا من أوراق الرموز غير القابلة للاستخدام (بسبب وظائف الأعضاء الافتراضية ، std :: string في واجهة برمجة التطبيقات العامة ، وما إلى ذلك). الشيء هو أن BWAPI مكتوب بلغة C ++. يصعب استخدام C ++ بشكل عام حتى من مشروعات C ++. بمجرد أن تكون المكتبة المترجمة أفضل للارتباط بنفس الرابط (الإصدارات المتطابقة) ، من الأفضل تحليل ملفات الرأس بنفس المترجم (الإصدارات المتطابقة). لأن هناك العديد من العوامل التي يمكن أن تؤثر على النتيجة. على سبيل المثال ، التشويش ، الذي لا يزال GNU GCC لا يمكنه تنفيذه بدون أخطاء . هذه العوامل مهمة للغاية بحيث لا يمكن التغلب عليها حتى في gtest ، وأشارت الوثائق إلى أنه سيكون من الأفضل لك بناء gtest كجزء من المشروع مع نفس المترجم ونفس الرابط.
Bwapi-c
لغة البرمجة C هي لغة مشتركة. إذا كان rust-bindgen يعمل بشكل جيد للغة C ، فلماذا لا تنفذ BWAPI لـ C ثم تستخدم API الخاص به؟ فكرة جيدة!
نعم ، إنها فكرة جيدة حتى تنظر إلى الشجاعة في BWAPI وترى عدد الفئات والأساليب التي تحتاج إلى تنفيذها = (خاصةً جميع هذه التخطيطات للهياكل في الذاكرة والمجمعين وتصحيحات الذاكرة والأهوال الأخرى التي ليس لدينا وقت لها. من الضروري استخدام الحل الحالي إلى أقصى حد.
ولكن يجب أن نتعامل بطريقة أو بأخرى مع mangling و C ++ code و الوراثة ووظائف الأعضاء الافتراضية.
في 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;
وجزء 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. كان المنتج الثانوي عبارة عن تجميع متقاطع والقدرة على تنفيذ API بأي لغة أخرى تدعم FFI واتفاقية استدعاء cdecl.
إذا كنت تكتب مكتبة ، يرجى كتابة C API.
أهم ميزة في BWAPI-C هي أوسع قدرة على الاندماج مع اللغات الأخرى. Python
و Ruby
و Rust
و PHP
و Java
والعديد من الآخرين يعرفون كيفية العمل مع C ، وبالتالي يمكنك أيضًا كتابة روبوت عليها ، إذا كنت تعمل قليلاً مع ملف وتقوم بتطبيق أغلفةك.
كتابة روبوت في C.
يصف هذا الجزء المبادئ العامة لتصميم وحدات Starcraft.
هناك نوعان من برامج التتبُّع: الوحدة النمطية والعميل. سننظر في مثال لكتابة وحدة نمطية.
الوحدة النمطية هي مكتبة قابلة للتنزيل ؛ يمكن رؤية المبدأ العام للتحميل هنا . يجب أن تصدر الوحدة newAIModule
: newAIModule
و gameInit
.
مع gameInit
كل شيء بسيط ، يتم استدعاء هذه الوظيفة لتمرير مؤشر للعبة الحالية. هذا المؤشر مهم جدا ، لأنه في براري BWAPI يعيش متغير ثابت عالمي يستخدم في بعض أجزاء الكود. دعونا وصف gameInit
:
DLLEXPORT void gameInit(void* game) { BWAPIC_setGame(game); }
newAIModule
أكثر تعقيدًا بعض الشيء. يجب أن ترجع مؤشر إلى فئة C ++ التي تحتوي على جدول أسلوب ظاهري بأسماء على XXXXX التي يتم استدعاؤها في أحداث لعبة معينة. تحديد هيكل الوحدة:
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
هو متغير ثابت في جدول الطريقة ، وقيم الطرق مليئة 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
. لذا ، سنعرض مثالًا على الترجمة المتقاطعة وتشغيل الوحدة:
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-sys
في نظام Rasta البيئي ، من المعتاد استدعاء الحزم بطريقة معينة ترتبط بالمكتبات الأصلية. أي حزمة foo-sys لها وظيفتان مهمتان:
- رابط إلى مكتبة libfoo الأصلية
- يقدم تعريفات الدوال من مكتبة libfoo. ولكن لا يتم توفير التصريحات فقط ، التجريد على مستوى عال في صناديق * -sys.
لكي تتمكن حزمة * -sys من الارتباط بنجاح ، تقوم بدمج بحث مكتبة أصلية و / أو تجميع مكتبة من المصادر إليها.
لكي تقوم حزمة * -sys بتقديم إعلانات ، يجب على المرء إما كتابتها يدويًا أو إنشاؤها باستخدام bindgen. مرة أخرى bindgen. المحاولة الثانية =)
توليد المجلدات باستخدام bwapi-c يصبح فاحشًا:
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.
على سبيل المثال ، للوظائف المعروفة بالفعل ، أنتج bindgen الإعلانات التالية:
extern "C" {
هناك استراتيجيتان: تخزين الكود الذي تم إنشاؤه في المستودع وإنشاء الكود على الطاير أثناء التجميع. كل من هذه الأساليب لها مزاياها وعيوبها .
مرحبًا بكم في bwapi-sys ، وهي خطوة صغيرة أخرى نحو هدفنا.
تذكر ، لقد تحدثت عن عبر منصة؟ انضم Nlinker إلى المشروع ونفذ استراتيجية صعبة. إذا كان الهدف هو Windows ، فقم بتنزيل BWAPIC الذي تم تجميعه بالفعل من github. وبالنسبة للأهداف المتبقية ، نقوم بجمع BWAPI-C من مصادر OpenBW (سأخبرك بعد ذلك بقليل).
bwapi-rs
الآن بعد أن أصبح لدينا المجلدات ، يمكننا وصف التجريدات عالية المستوى. لدينا نوعان للعمل بهما: قيم نقية ومؤشرات معتمة.
مع القيم النقية ، كل شيء أبسط. خذ اللون كمثال. نحن بحاجة إلى جعله مناسبًا لاستخدام رمز Rust حتى نتمكن من استخدام الألوان بطريقة مريحة وطبيعية:
game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red); ^^^
لذلك من أجل الاستخدام المريح ، سيكون من الضروري تحديد لغة اصطلاحية لتعداد لغة الصدأ مع ثوابت من C ++ وتحديد طرق التحويل في bwapi_sys :: Color باستخدام std :: convert :: From trait:
على الرغم من الراحة ، يمكنك استخدام قفص التعداد البدائي المشتق .
مع مؤشرات مبهمة ، لن يكون الأمر أكثر صعوبة. للقيام بذلك ، استخدم نمط Newtype :
pub struct Player(*mut sys::Player);
أي أن Player هو بنية معينة ذات مجال خاص - مؤشر معتم خام من C. وهنا يمكنك وصف طريقة Player :: color:
impl Player {
الآن يمكننا كتابة أول روبوت لدينا في Rust!
كتابة بوت في الصدأ
كدليل على المفهوم ، سيبدو البوت كبلد مشهور واحد: كل وظائفه ستكون توظيف العمال وجمع المعادن.


لنبدأ gameInit
المطلوبة 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]
تؤدي نفس الوظيفة التي يؤديها #[no_mangle]
extern "C"
في 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 ، وهو تناظري للجدول الظاهري AIModule_vtable من C:
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 ... ... ...
وفيديو العمل:
قليلا عن الترجمة المتقاطعة
باختصار ، الصدأ جميل! بنقرتين ، يمكنك وضع العديد من سلاسل الأدوات لمنصات مختلفة. على وجه التحديد ، يتم تعيين سلسلة أدوات i686-pc-windows-gnu بواسطة الأمر:
rustup target add i686-pc-windows-gnu
يمكنك أيضًا تحديد kofig للشحن في جذر مشروع .cargo/config
:
[target.i686-pc-windows-gnu] linker = "i686-w64-mingw32-gcc" ar = "i686-w64-mingw32-ar" runner = "wine"
وهذا كل ما عليك فعله لتجميع مشروع Rust الخاص بك من Linux على Windows.
Openbw
ذهب هؤلاء الرجال أبعد من ذلك. قرروا كتابة نسخة مفتوحة المصدر للعبة SC: BW! وهم يقومون بعمل جيد. كان أحد أهدافهم تنفيذ صور عالية الدقة ، ولكن SC: Remastered تسبقهم = (في الوقت الحالي ، يمكنك استخدام واجهة برمجة التطبيقات الخاصة بهم لكتابة برامج الروبوت (نعم ، أيضًا في C ++). ولكن الميزة الأكثر إثارة للدهشة هي القدرة على عرض الإعادة مباشرة في المتصفح .
الخلاصة
ظلت مشكلة لم يتم حلها أثناء التنفيذ: نحن لا نتحكم في تفرد الروابط ، وسيؤدي الوجود المتزامن لـ &mut
&
عند تغيير كائن إلى سلوك غير محدد. مشكلة. حاول التوقف تنفيذ الارتباطات الاصطلاحية ، لكن فتيله تلاشى قليلاً. أيضًا ، لحل هذه المشكلة ، سيكون عليك تجريف واجهة برمجة تطبيقات C ++ وتعيين مؤهلات const
بشكل صحيح.
لقد استمتعت حقًا بالعمل في هذا المشروع ، شاهدت الإعادة وانغمس بعمق في الغلاف الجوي. تركت هذه اللعبة 믿어. 않을 정도 인 إرثًا. لا توجد لعبة مشهورة مع SC: BW ، وتأثيرها على 대한민국 정치 에게 كان لا يمكن تصوره. إن اللاعبين المحترفين في كوريا popular يحظون بشعبية مثل الدراما الكورية التي تبث في وقت الذروة. 또한، 한국 에서 프로 게이머 라면 군대 의 특별한 육군 에 입대 할 수 있다.
يعيش StarCraft!
المراجع