Processing the game objects in multithread manner.

วันนี้พูดถึงเรื่องการเขียนเกมสักหน่อย พอดีว่าผมเคยเขียนเกมมาก่อนตอนสมัยทำงานใหม่ ๆ และตอนนี้ผมเริ่มอยากกลับมาเขียนเกมอีกแล้วก็เลยต้องรื้อฟื้นสักหน่อยครับ

Game Object ก็คือวัตถุที่อยู่ในเกม เป็นส่วนของลอจิคของเกม ตรงนี้จะรวมตั้งแต่ ตัวละครที่ผู้เล่นบังคับ ตัวละครที่ผู้เล่นไม่ได้บังคับ (NPC ทั้งหลาย รวมทั้งพวกศัตรู) วัตถุพวกกระสุนปืน หรือแม้กระทั่งฉากและอื่น ๆ เราจะมองว่าโปรแกรมเกมคือชุดของวัตถุในเกมและการปฎิสัมพันธ์กันระหว่างวัตถุก็ได้ครับ

โดยทั่วไป โปรแกรมประเภทเกมจะมีลักษณะแบบนี้ครับ

while(true) {
  Input input = ReadInput();
  ProcessGameObjects(input);
  DrawGameObjects();
}

ผมละรายละเอียดเกี่ยวกับการออกจากลูปนะครับ

ในอดีตขณะที่เรามี CPU แกนเดียว การใช้ Thread เดียวร่วมกันทั้งโปรแกรมเป็นวิธีที่ดีที่สุด เพราะว่าต่อให้เราแยก Thread ไปมันก็ต้องสลับกันทำงานไปมา Performance ก็แย่ลง และไม่มีข้อดีอะไรจากการแยก Thread ดังนั้นเขียนโปรแกรมเป็นอนุกรมจะง่ายที่สุด

ดังนั้นฟังก์ชั่น ProcessGameObjects() ก็จะมีหน้าตาประมาณนี้ครับ

void ProcessGameObjects(const Input& input) {
  for(auto& obj : GetGameObjects()) {obj.Process(input);}
}

วิธีนี้จริง ๆ ไม่ได้มีข้อเสียอะไรนะ เพียงแต่ว่าโปรแกรมจะทำงานอยู่บนแกนเดียวของ CPU ซึ่งนั่นคือการไม่ได้ใช้งานอีก 75% ที่เหลือของระบบที่มี 4 แกน มันก็ดูน่าเสียดายนิดหน่อยล่ะครับ

ผมเคยคิดว่า ถ้าเราจะแยกให้แต่ละ object ทำงานอยู่บน thread ของตัวเองไปเลยจะดีมั้ย แล้วก็แยกออกมาจาก Render Loop ไปเลยด้วย คราวนี้เกมของเราก็จะมี thread เต็มไปหมด เกิดผมมีวัตถุในเกมประมาณสัก 100 ชิ้น ก็จะมี 100 thread ทำงานพร้อมๆ กัน

ตัว Game Object ก็อาจจะมีสภาพเป็น ... แบบนี้

void GameObject::Process() {
  while(true) {
    Input input = ReadInput();
    switch(input.type) {
       //calculating things.
    }
  }   
}

CreateThread([](gameobject.Process());).run();    
while(true) {
  DrawGameObjects();
}

ปัญหาของวิธีนี้คือคราวนี้เราจะมี thread เยอะมากจนควบคุมได้ยาก มีปัญหาเรื่อง synchronization ระหว่าง thread ที่วาดวัตถุขึ้นจอ กับ thread ของการประมวลผล และความจริงคือการสลับ thread ไปมาก็มี penalty เช่นกัน คราวนี้กลายเป็นว่ามี thread เยอะเกินไปจนทำให้ตัวโปรแกรมช้าลง

วิธีข้างบนผมไม่แนะนำให้ใช้นะครับ ปวดหัว 555

วิธีต่อไปจะเป็นการเปลี่ยน Game Loop แบบธรรมดาให้เป็น multi-thread แทน เป็นวิธีง่าย ๆ ครับ

while(true) {
  Input input = ReadInput();
  ProcessGameObjects(input);
  DrawGameObjects();
}

void ProcessGameObjects(const Input& input) {
  ThreadPool threadPool;
  for(auto& obj : GetGameObjects()) {
    threadPool.CreateThread([&input](){obj.Process(input);});
  }
  threadPool.WaitForAll();
}

วิธีนี้เป็นการแยกการคำนวนแต่ละวัตถุไปเป็นแต่ละ thread สั่งให้มันทำงาน แล้วรอจนกว่ามันจะทำงานเสร็จ จากนั้นก็ค่อยวาดแต่ละวัตถุขึ้นจอ วิธีนี้ดีตรงที่ว่ามันควบคุมได้ง่ายกว่ามาก ถ้าเราจะหยุดไม่ให้การคำนวนวัตถุทำงาน ก็แค่ไม่เรียก Process() ให้ทำงาน แค่นั้นเอง (เทียบกับวิธีบนที่ต้องสร้าง flag ขึ้นมาใหม่แล้วปวดหัวกว่ามาก) แต่ปัญหาอื่น ๆ ก็ยังมีมาให้ปวดหัวอยู่บ้างเหมือนกัน แต่ผมคิดว่าอันนี้ทำงานกับมันง่ายกว่าครับ

มันก็ยังมีประเด็นเรื่องของจำนวน thread มากเกินไป และการสลับไปมานั้นมันก็ค่อนข้างแพง ผมคิดว่าการบังคับให้มีจำนนวน thread น้อย ๆ นั้นจะให้ผลที่ดีกว่าเพราะว่าเรามีจำนวนแกนของซีพียูจำกัดนั่นแหละ

void ProcessGameObjects(const Input& input) {
  constexpr int maxThreadCount = 4;

  ThreadList threadPool;
  auto gameObjects = GetGameObjects();
  auto iter = gameObjects.first();

  while(iter != gameObjects.last()) {
    if(threadList.GetActiveThreadCount() < maxThreadCount && iter != gameObjects.end() {
      auto &object = *iter;
      threadPool.CreateThread([&input](){object.Process(input);});
      ++iter;
    }
  }
  threadPool.WaitForAll();
}

อันนี้คือการจำกัดไม่ให้มี thread เกินกว่า 4 thread ทำงานพร้อม ๆ กันในเวลาเดียว ซึ่งจะทำงานกับระบบที่มี 4 แกนได้ดีกว่าการทำงานทีเดียวพร้อม ๆ กันเป็น 100 ครับ :)

ข้อเสียคือมันก็ยังมีเรื่องของ Synchronization ให้ปวดหัวอยู่ดีนั่นแหละ แต่ว่ามันจะง่ายกว่าการแยกออกจาก draw thread ไปเลยมากครับ (แต่การแยกออกจาก draw thread ไปเลยก็มีข้อดีของมันนะ)

Wutipong Wongsakuldej

Programmer, interested in frontend applications, music and multimedia.

Latest posts by Wutipong Wongsakuldej (see all)

Leave a Reply

Your email address will not be published. Required fields are marked *