ในบทความนี้ ฉันจะพูดถึงประสบการณ์ส่วนตัวในการพัฒนาเกมเล็ก ๆ ใน Rust ใช้เวลาประมาณ 24 ชั่วโมงในการสร้างเวอร์ชันที่ใช้งานได้ (ส่วนใหญ่ฉันทำงานในตอนเย็นหรือวันหยุดสุดสัปดาห์) เกมยังไม่จบ แต่ฉันคิดว่าประสบการณ์จะคุ้มค่า ฉันจะแบ่งปันสิ่งที่ฉันเรียนรู้และข้อสังเกตบางอย่างที่ฉันทำในขณะที่สร้างเกมตั้งแต่เริ่มต้น
Skillbox แนะนำ: หลักสูตรภาคปฏิบัติสองปี
"ฉันเป็นนักพัฒนาเว็บ PRO" .เราเตือนคุณ: สำหรับผู้อ่าน "Habr" ทุกคน - ส่วนลด 10 rubles เมื่อลงทะเบียนในหลักสูตร Skillbox ใด ๆ โดยใช้รหัสส่งเสริมการขาย "Habr"
ทำไมต้องเป็นสนิม?
ฉันเลือกภาษานี้เพราะฉันได้ยินเรื่องดีๆ มากมายเกี่ยวกับภาษานี้ และฉันเห็นว่าภาษานี้ได้รับความนิยมมากขึ้นเรื่อยๆ ในการพัฒนาเกม ก่อนที่จะเขียนเกม ฉันมีประสบการณ์เพียงเล็กน้อยในการพัฒนาแอพพลิเคชั่นง่ายๆ ใน Rust แค่นี้ก็เพียงพอแล้วที่จะทำให้ฉันรู้สึกมีอิสระในขณะที่เขียนเกม
ทำไมต้องเป็นเกมและเป็นเกมประเภทไหน?
การสร้างเกมเป็นเรื่องสนุก! ฉันหวังว่าจะมีเหตุผลมากกว่านี้ แต่สำหรับโครงการ "บ้าน" ฉันเลือกหัวข้อที่ไม่เกี่ยวข้องกับงานประจำของฉันมากเกินไป นี่คือเกมอะไร? ฉันต้องการสร้างบางอย่างเช่นเครื่องจำลองเทนนิสที่ผสมผสานระหว่าง Cities Skylines, Zoo Tycoon, Prison Architect และเทนนิสเข้าด้วยกัน โดยทั่วไปแล้วกลายเป็นเกมเกี่ยวกับสถาบันสอนเทนนิสที่มีคนมาเล่น
การฝึกอบรมทางเทคนิค
ฉันต้องการใช้ Rust แต่ฉันไม่รู้ว่าจะต้องใช้พื้นฐานมากเพียงใดในการเริ่มต้น ฉันไม่ต้องการเขียนตัวเชเดอร์พิกเซลและใช้การลากและวาง ดังนั้นฉันจึงมองหาโซลูชันที่ยืดหยุ่นที่สุด
ฉันพบแหล่งข้อมูลที่เป็นประโยชน์ที่ฉันแบ่งปันกับคุณ:
เราเล่นยัง — รายการองค์ประกอบ Rust ที่จำเป็นสำหรับการพัฒนาเกมsubreddit dev เกมสนิม; ศิลปะพิกเซลฟรี
ฉันสำรวจเอ็นจิ้นเกม Rust หลายตัว ท้ายที่สุดก็เลือก Piston และ ggez ฉันเจอพวกเขาขณะทำงานในโครงการก่อนหน้านี้ ท้ายที่สุด ฉันเลือก ggez เพราะมันดูเหมาะสมกว่าสำหรับการนำเกม 2D ขนาดเล็กไปใช้ โครงสร้างโมดูลาร์ของ Piston นั้นซับซ้อนเกินไปสำหรับนักพัฒนามือใหม่ (หรือผู้ที่ทำงานกับ Rust เป็นครั้งแรก)
โครงสร้างเกม
ฉันใช้เวลาคิดเกี่ยวกับสถาปัตยกรรมของโครงการอยู่พักหนึ่ง ขั้นตอนแรกคือการสร้าง "ที่ดิน" ผู้คน และสนามเทนนิส ประชาชนต้องเดินไปรอบสนามและรอ ผู้เล่นจะต้องมีทักษะที่พัฒนาขึ้นเมื่อเวลาผ่านไป นอกจากนี้ควรมีโปรแกรมแก้ไขที่ให้คุณเพิ่มบุคคลใหม่และศาลได้ แต่นี่ไม่ฟรีอีกต่อไป
เมื่อคิดทุกอย่างแล้วฉันก็เริ่มทำงาน
การสร้างเกม
จุดเริ่มต้น: วงกลมและนามธรรม
ฉันเอาตัวอย่างจาก ggez และได้วงกลมบนหน้าจอ มหัศจรรย์! ตอนนี้มีนามธรรมบางอย่าง ฉันคิดว่ามันคงจะดีถ้าเป็นนามธรรมออกไปจากแนวคิดของวัตถุในเกม แต่ละออบเจ็กต์จะต้องถูกเรนเดอร์และอัปเดตตามที่ระบุไว้ที่นี่:
// the game object trait
trait GameObject {
fn update(&mut self, _ctx: &mut Context) -> GameResult<()>;
fn draw(&mut self, ctx: &mut Context) -> GameResult<()>;
}
// a specific game object - Circle
struct Circle {
position: Point2,
}
impl Circle {
fn new(position: Point2) -> Circle {
Circle { position }
}
}
impl GameObject for Circle {
fn update(&mut self, _ctx: &mut Context) -> GameResult<()> {
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
let circle =
graphics::Mesh::new_circle(ctx, graphics::DrawMode::Fill, self.position, 100.0, 2.0)?;
graphics::draw(ctx, &circle, na::Point2::new(0.0, 0.0), 0.0)?;
Ok(())
}
}
โค้ดชิ้นนี้ให้รายการออบเจ็กต์ที่ดีแก่ฉันซึ่งฉันสามารถอัปเดตและเรนเดอร์ในลูปที่ดีไม่แพ้กัน
mpl event::EventHandler for MainState {
fn update(&mut self, context: &mut Context) -> GameResult<()> {
// Update all objects
for object in self.objects.iter_mut() {
object.update(context)?;
}
Ok(())
}
fn draw(&mut self, context: &mut Context) -> GameResult<()> {
graphics::clear(context);
// Draw all objects
for object in self.objects.iter_mut() {
object.draw(context)?;
}
graphics::present(context);
Ok(())
}
}
main.rs เป็นสิ่งจำเป็นเนื่องจากมีโค้ดทุกบรรทัด ฉันใช้เวลาเล็กน้อยในการแยกไฟล์และปรับโครงสร้างไดเร็กทอรีให้เหมาะสม นี่คือสิ่งที่ดูเหมือนหลังจากนั้น:
ทรัพยากร -> นี่คือที่ซึ่งเนื้อหาทั้งหมดอยู่ (รูปภาพ)
สิ่งอำนวยความสะดวก
- เอนทิตี
— game_object.rs
— วงกลม.อาร์เอส
— main.rs -> ลูปหลัก
ผู้คน พื้น และรูปภาพ
ขั้นตอนต่อไปคือการสร้างวัตถุเกม Person และโหลดรูปภาพ ทุกอย่างควรสร้างจากกระเบื้องขนาด 32*32 แผ่น
สนามเทนนิส
หลังจากศึกษาลักษณะของสนามเทนนิสแล้ว ฉันก็ตัดสินใจสร้างจากกระเบื้องขนาด 4*2 ในตอนแรกคุณสามารถสร้างภาพขนาดนี้หรือรวม 8 แผ่นแยกกัน แต่แล้วฉันก็พบว่ามีเพียงสองแผ่นที่ไม่ซ้ำใครเท่านั้นที่จำเป็น และนี่คือเหตุผล
โดยรวมแล้วเรามีไพ่สองใบดังกล่าว: 1 และ 2
แต่ละส่วนของสนามประกอบด้วยช่อง 1 หรือช่อง 2 สามารถวางได้ตามปกติหรือพลิก 180 องศา
โหมดการก่อสร้างขั้นพื้นฐาน (การประกอบ)
หลังจากที่ฉันจัดการเรนเดอร์ไซต์ ผู้คน และแผนที่ได้สำเร็จ ฉันพบว่าจำเป็นต้องมีโหมดการประกอบพื้นฐานด้วย ฉันนำไปใช้เช่นนี้: เมื่อกดปุ่ม วัตถุจะถูกเลือก และการคลิกจะวางวัตถุนั้นในตำแหน่งที่ต้องการ ดังนั้นปุ่ม 1 ให้คุณเลือกสนามได้ และปุ่ม 2 ให้คุณเลือกผู้เล่นได้
แต่เรายังต้องจำไว้ว่า 1 และ 2 หมายถึงอะไร ดังนั้นฉันจึงเพิ่มโครงร่างเพื่อให้ชัดเจนว่าวัตถุใดถูกเลือก นี่คือสิ่งที่ดูเหมือน
คำถามเกี่ยวกับสถาปัตยกรรมและการปรับโครงสร้างใหม่
ตอนนี้ฉันมีวัตถุในเกมหลายอย่าง: ผู้คน, สนามและพื้น แต่เพื่อให้ wireframes ทำงานได้ แต่ละเอนทิตีของออบเจ็กต์จะต้องได้รับการบอกว่าออบเจ็กต์นั้นอยู่ในโหมดสาธิตหรือว่าเฟรมนั้นถูกวาดอย่างง่าย ๆ หรือไม่ ซึ่งไม่สะดวกมากนัก
สำหรับฉันดูเหมือนว่าสถาปัตยกรรมจำเป็นต้องได้รับการปรับปรุงใหม่ในลักษณะที่เปิดเผยข้อจำกัดบางประการ:
- การมีเอนทิตีที่เรนเดอร์และอัปเดตตัวเองนั้นเป็นปัญหา เนื่องจากเอนทิตีนั้นจะไม่สามารถ "รู้" ว่าควรจะเรนเดอร์อะไร เช่น รูปภาพและโครงร่าง
- ขาดเครื่องมือสำหรับการแลกเปลี่ยนคุณสมบัติและพฤติกรรมระหว่างเอนทิตีแต่ละรายการ (เช่น คุณสมบัติ is_build_mode หรือการแสดงพฤติกรรม) มันเป็นไปได้ที่จะใช้การสืบทอดแม้ว่าจะไม่มีวิธีที่เหมาะสมในการนำไปใช้ใน Rust ก็ตาม สิ่งที่ฉันต้องการจริงๆ คือเลย์เอาต์
- จำเป็นต้องมีเครื่องมือสำหรับการมีปฏิสัมพันธ์ระหว่างหน่วยงานเพื่อมอบหมายบุคคลให้ขึ้นศาล
- เอนทิตีเองเป็นส่วนผสมของข้อมูลและตรรกะที่ไม่สามารถควบคุมได้อย่างรวดเร็ว
ฉันค้นคว้าเพิ่มเติมและค้นพบสถาปัตยกรรม
ECS - ระบบส่วนประกอบเอนทิตี ซึ่งใช้กันทั่วไปในเกม นี่คือประโยชน์ของ ECS:
- ข้อมูลถูกแยกออกจากตรรกะ
- องค์ประกอบแทนมรดก
- สถาปัตยกรรมที่เน้นข้อมูลเป็นศูนย์กลาง
ECS โดดเด่นด้วยแนวคิดพื้นฐานสามประการ:
- เอนทิตี - ประเภทของวัตถุที่ตัวระบุอ้างถึง (อาจเป็นผู้เล่น ลูกบอล หรืออย่างอื่น)
- ส่วนประกอบ - เอนทิตีประกอบด้วยส่วนประกอบเหล่านั้น ตัวอย่าง - องค์ประกอบการเรนเดอร์ สถานที่ และอื่นๆ เหล่านี้คือคลังข้อมูล
- ระบบ - ใช้ทั้งอ็อบเจ็กต์และส่วนประกอบ รวมถึงพฤติกรรมและตรรกะที่อิงตามข้อมูลนี้ ตัวอย่างคือระบบการเรนเดอร์ที่วนซ้ำเอนทิตีทั้งหมดด้วยส่วนประกอบการเรนเดอร์และทำการเรนเดอร์
หลังจากศึกษาแล้ว เห็นได้ชัดว่า ECS สามารถแก้ไขปัญหาต่อไปนี้ได้:
- การใช้โครงร่างแทนการสืบทอดเพื่อจัดระเบียบเอนทิตีอย่างเป็นระบบ
- การกำจัดโค้ดที่สับสนผ่านระบบควบคุม
- การใช้วิธีการเช่น is_build_mode เพื่อให้ลอจิกโครงร่างอยู่ในตำแหน่งเดียวกัน - ในระบบการเรนเดอร์
นี่คือสิ่งที่เกิดขึ้นหลังจากนำ ECS ไปใช้
ทรัพยากร -> นี่คือที่ซึ่งเนื้อหาทั้งหมดอยู่ (รูปภาพ)
สิ่งอำนวยความสะดวก
- ส่วนประกอบ
—position.rs
— person.rs
— Tennis_court.rs
— floor.rs
- wireframe.rs
— mouse_tracked.rs
- ทรัพยากร
—mouse.rs
- ระบบ
— rendering.rs
— ค่าคงที่.rs
— utils.rs
— world_factory.rs -> ฟังก์ชั่นโรงงานโลก
— main.rs -> ลูปหลัก
เรามอบหมายคนให้กับศาล
ECS ทำให้ชีวิตง่ายขึ้น ตอนนี้ฉันมีวิธีที่เป็นระบบในการเพิ่มข้อมูลให้กับเอนทิตีและเพิ่มตรรกะตามข้อมูลนั้น และนี่ก็ทำให้สามารถจัดระเบียบการกระจายตัวของผู้คนในศาลได้
ฉันทำอะไรลงไป:
- เพิ่มข้อมูลเกี่ยวกับศาลที่ได้รับมอบหมายให้กับบุคคล
- เพิ่มข้อมูลเกี่ยวกับผู้คนที่กระจายไปยัง TennisCourt;
- เพิ่ม CourtChoosingSystem ซึ่งช่วยให้คุณวิเคราะห์ผู้คนและศาล ตรวจจับสนามที่มีอยู่ และกระจายผู้เล่นให้พวกเขา
- เพิ่ม PersonMovementSystem ซึ่งจะค้นหาบุคคลที่ถูกกำหนดให้อยู่ในศาล และหากไม่มีก็จะส่งบุคคลไปยังที่ที่ต้องการ
ข้อสรุปถึง
ฉันสนุกกับการเล่นเกมง่ายๆ นี้มาก ยิ่งกว่านั้น ฉันดีใจที่ฉันใช้ Rust ในการเขียน เพราะ:
- สนิมให้สิ่งที่คุณต้องการ
- มีเอกสารที่ยอดเยี่ยม Rust ค่อนข้างหรูหรา
- ความสม่ำเสมอนั้นเจ๋ง
- คุณไม่จำเป็นต้องหันไปพึ่งการโคลน การคัดลอก หรือการกระทำอื่นที่คล้ายกัน ซึ่งฉันมักจะทำในภาษา C++
- ตัวเลือกใช้งานง่ายมากและจัดการกับข้อผิดพลาดได้เป็นอย่างดี
- หากสามารถคอมไพล์โปรเจ็กต์ได้ ก็จะทำงานได้ 99% และตรงตามที่ควร ฉันคิดว่าข้อความแสดงข้อผิดพลาดของคอมไพเลอร์ดีที่สุดที่ฉันเคยเห็น
การพัฒนาเกมใน Rust เพิ่งเริ่มต้น แต่มีชุมชนที่มั่นคงและค่อนข้างใหญ่อยู่แล้วที่ทำงานเพื่อเปิด Rust ให้กับทุกคน ดังนั้นฉันจึงมองอนาคตของภาษาด้วยการมองโลกในแง่ดี และรอคอยผลงานร่วมกันของเรา
Skillbox แนะนำ:
- คอร์สออนไลน์
“นักพัฒนาส่วนหน้ามืออาชีพ” .- หลักสูตรภาคปฏิบัติ
"นักพัฒนามือถือ PRO" .- หลักสูตรปีปฏิบัติ
"นักพัฒนา PHP จาก 0 ถึง PRO" .
ที่มา: will.com