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 ไปเลยก็มีข้อดีของมันนะ)

สิ่งที่ “โปรแกรมเมอร์” ต้องการ ?

โพสต์นี้เป็นการตอบกลับบล็อกของคุณ khachoji 5 สิ่งที่เจ้านายควรเข้าใจ “โปรแกรมเมอร์” ให้มากกว่านี้ คือ ผมมีความเห็นใกล้เคียงกับคุณเอ็มระดับหนึ่งนะ ก็เลยว่าจะเขียนเรื่องนี้บ้าง (ว่ากันง่าย ๆ คือก็ลอกการบ้านนั่นแหละ)

ก่อนอื่นจะขอเริ่มจากเรื่องที่ไม่เกี่ยวข้องกันก่อน ส่วนตัวผมจะแบ่งโปรแกรมเมอร์ออกเป็น 2 กลุ่ม

  1. พวก Shinso หรือพวกเลือดแท้ พวกนี้เป็นโปรแกรมเมอร์กันถึงระดับวิญญาณ เป็นพวกที่มีชีวิตอยู่เพื่อการสร้างสรรค์ผลงาน มีแรงบันดาลใจเป็นแรงขับเคลื่อน
  2. พวก Shito หรือพวกสาวก พวกนี้จริง ๆ แล้วไม่ได้อยากเป็นโปรแกรมเมอร์กันหรอก แต่ว่าถูกความสำเร็จของพวกเลือดแท้ดึงดูดเข้ามา มีผลประโยชน์เป็นแรงขับเคลื่อน

เอ่อ … ชักจะเริ่มเลอะเทอะไปใหญ่ (ศัพท์พวกนี้มาจากเกม Tsukihime ครับ) คือผมจะบอกว่าจริง ๆ แล้วคนที่เป็นโปรแกรมเมอร์เองก็มักจะแบ่งเป็นคนที่ชอบการสร้างสรรค์ผลงาน ทำเพราะว่ามันสนุก ทำเพราะว่าใจรัก กับคนที่ไม่ใช่ ซึ่งสิ่งที่ผมจะพูดถึงต่อไปเนี่ยคิดว่ามันจะเกี่ยวกับคนกลุ่มแรกมากกว่ากลุ่มที่สอง

ส่วนผมเป็นพวกกลุ่มไหนเหรอ ? … เอ่อ ไม่รู้สิ ผมบอกได้แค่ว่าถ้าผมไม่เขียนโปรแกรม ก็นึกไม่ออกเหมือนกันว่าจะทำอะไร เพราะว่าเขียนอยู่ทุกวัน 555

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

พื้นที่

คำว่า “พื้นที่” ที่ผมพูดถึง ไม่ใช่แค่พื้นที่ในเชิงรูปธรรม แต่หมายรวมทั้งพื้นที่ในเชิงนามธรรมด้วย

งานเขียนโปรแกรมเป็นงานศิลปะครับ คือ ตัวโปรแกรม หรือตัวคอมพิวเตอร์ มันเป็นผลงานวิศวกรรมก็จริงนะ แต่ว่าการเขียนโค๊ดเนี่ยมันจะมีการตัดสินใจเข้ามาเกี่ยวข้องมากมาย (เพราะเรากำลังสั่งให้คอมพิวเตอร์ทำงานอยู่น่ะครับ) และการตัดสินใจเนี่ยมันเป็นศิลปะที่ยากที่สุดแขนงนึงเลยนะ ดังนั้นพวกเราโปรแกรมเมอร์ในระดับหนึ่งก็เป็นศิลปินอยู่ และการทำงานศิลปะนั้นต้องใช้พื้นที่ค่อนข้างกว้างกว่างานหลาย ๆ ประเภท

ดังนั้น ในแง่ของพื้นที่ สิ่งแรกที่โปรแกรมเมอร์ต้องการคือ “อำนาจการตัดสินใจ” ครับ เพราะว่าเราต้องตัดสินใจในเรื่องมากมาย (แม้กระทั่งการตั้งชื่อลูก!) ดังนั้นถ้าเกิดว่ามีเรื่องใดเรื่องหนึ่งที่ต้องพึ่งพาอาศัยคนอื่นในการตัดสินใจ เช่น การเปลี่ยนพฤติกรรมของโปรแกรม ถ้าเกิดว่าเราต้องรอคนอื่นงานมันจะล้าช้ามาก และนั่นคือสิ่งสุดท้ายที่คุณต้องการใช่ไหมครับ

สิ่งที่สองก็คือ “สมาธิ” อย่างที่บอกว่าการเขียนโปรแกรมมีการตัดสินใจเข้ามาเกี่ยวข้องเยอะมาก โปรแกรมเมอร์จะต้องคิด จะต้องหาข้อมูลสนับสนุน จะต้องหาข้อมูลประกอบการตัดสินใจ และทั้งหมดเป็นงานที่ต้องใช้สมาธิมหาศาล ฉะนั้นถ้าเกิดว่าคุณทำให้โปรแกรมเมอร์สติหลุดไประหว่างที่กำลังอยู่ในห้วงความคิดนิดเดียว ความคิดตรงนั้นก็จะหลุดหายไปและจะต้องเริ่มคิดใหม่ตั้งแต่ต้น (ซึ่งบ้างครั้งนั่นหมายถึง 15-30 นาทีเลยทีเดียว) แน่นอนว่ามีโปรแกรมเมอร์ที่มีสมาธิแข็ง ๆ จะสามารถสลับความคิดไปมา หรือสามารถกู้คืนความคิดได้อย่างรวดเร็ว แต่โปรแกรมเมอร์ส่วนใหญ่ที่ผมเห็นทำไม่ค่อยได้นะ

สิ่งที่สามคือ “เวลา” ผมเป็นพวกจะต้องหยุดพักทุก ๆ 1-2 ชั่วโมง คือถ้าทำงานต่อเนื่องงานจะออกมาแย่มาก เพราะมันเป็นงานที่ใช้พลังเยอะมาก ผมจะต้องออกไปเดินพักบ้างทุก ๆ 1-2 ชั่วโมง (ถ้าเป็นงานบริษัทผมจะชดเชยโดยการทำงานให้ยาวขึ้นโดยไม่เก็บ OT น่ะนะ) ส่วนอีกหลาย ๆ คนที่ผมเห็นก็จะมีการหยุดพักด้วยวิธีอื่น ๆ เช่น โทรศัพท์คุยกับแฟนบ้าง กดไอแพดเล่นบ้าง หรือแม้กระทั่งเล่นเกมในเวลางาน

ความเข้าใจ

คนมักจะคิดว่าโปรแกรมเมอร์ส่วนใหญ่เป็นคนเข้าใจยาก เพราะเราใช้ศัพท์เฉพาะทางเยอะ และพวกเราส่วนใหญ่ใช้ภาษาอังกฤษได้ดีถึงดีโคตร ๆ (ประชดบ้างนิดหน่อย) ดังนั้นหลาย ๆ ครั้งเราจะพูดไทยคำอังกฤษคำจนคนมักจะคิดว่าเรากระแดะ

คุณอาจจะเจอโปรแกรมเมอร์ที่ติ๊สมาก ๆ จนถึงคนที่แบบทำตัวเหมือนเดิมทุกวัน ๆ จนเหมือนกับหุ่นยนต์

แต่ก็ไม่ใช่ว่าคนพวกนี้ไม่ต้องการความเข้าใจ ไม่ใช่ว่าเขาต้องการที่จะอยู่คนเดียวแบบไม่สนใจคนรอบข้าง เพียงแต่ว่างานของโปรแกรมเมอร์นั้นจะคล้าย ๆ กับการทำงานกับทาส (ก็โปรแกรม/คอมพิวเตอร์นั่นแหละ) ดังนั้นเวลาเราสั่งงานไปก็จะไม่ต้องไปทำความเข้าใจกับความรู้สึกของคอมพิวเตอร์ เราไม่ต้องไปคิดว่ามันจะน้อยใจหรือมันจะป่วยหรืออะไร ก็เลยกลายเป็นว่าทักษะความเป็นมนุษย์ของโปรแกรมเมอร์หลาย ๆ คนนั้นออกจะต่ำกว่ามาตรฐานคนทั่วไประดับหนึ่ง (อันนี้ขึ้นกับพื้นฐานครอบครัวด้วยนะครับ ไม่ได้เป็นกันทุกคน) การแสดงออกของโปรแกรมเมอร์หลาย ๆ คนจึงออกไปในทางตรง ๆ แรง ๆ เพราะกับคอมพิวเตอร์มันเป็นวิธีที่ได้ผลดีที่สุด แต่กับมนุษย์นั้นมันมีผลกระทบทางอ้อมมากมายเช่นกัน

ถ้าเกิดคุณดันดวงไม่ดีมีแฟนเป็นโปรแกรมเมอร์ ก็คงต้องทำความเข้าใจกันให้ดี ลองศึกษานิสัยใจคอและทำความเข้าใจกับสิ่งที่เขาเป็น ถ้าเกิดว่ารับไม่ได้คุณอาจจะต้องถอนตัวออกไป แต่ผมพบว่าโปรแกรมเมอร์ส่วนใหญ่นิสัยน่ารักนะ (แต่การแสดงออกอาจจะสอบตกไปสองสามคะแนน)

แรงสนับสนุน

ความยากของงานเขียนโปรแกรมจริง ๆ อยู่ที่การติดตามพัฒนาการของวงการนี้ครับ คือ ในหลาย ๆ วงการการพัฒนาเนี่ยอาจจะเป็นรายปี อาจจะแบบใช้เวลา 1 ปีในการออกแบบเครื่องมือ 1 ปีผลิตเครื่องมือ ทดลองใช้กับสถานการณ์จริง 5 ปี ก่อนจะเริ่มขาย ตอนขายกว่าคนจะซื้อไปใช้ก็อีก 2-3 ปี งานด้านไอทีเนี่ยทุกอย่างรวมกันใช้เวลาไม่ถึงปีถึง 2-3 ปีเท่านั้นเอง ดังนั้นในแต่ละวันจะมีอะไรใหม่ๆ ออกมาเยอะมาก ยิ่งถ้าเป็นเทคโนโลยีที่เป็นเทรนด์ใหม่ ๆ นี่จะวิ่งเร็วแบบตามไม่ทันกันเลย

โปรแกรมเมอร์แต่ละคนจึงต้องเรียนรู้และพัฒนาตัวเองแบบวันต่อวัน คุณจะเห็นโปรแกรมเมอร์ที่หลังเลิกงานก็เอาแต่เขียนโปรแกรมแทบเป็นแทบตาย หรือไม่ก็นั่งอ่านหนังสือตลอดเวลาว่างที่มี เพราะมันมีสิ่งที่ต้องเรียนรู้มากมายในแต่ละวัน

ดังนั้นถ้ามีแรงสนับสนุนให้เขาสามารถพัฒนาตัวเองได้ง่ายและดีขึ้นละก็ โปรแกรมเมอร์รักตายเลย 55

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

คำว่ามากน่ะแค่ไหน ? ผมเคยทำงานที่ใช้เวลา 2 ชม.ให้เสร็จภายใน 30 วินาทีมาแล้ว 😉 (ก็คือการเขียนสคริปท์ขึ้นมาแทนงานทำมือนั่นล่ะครับ ส่วนที่ช้าที่สุดของการทำงานกับโปรแกรมคือส่วนของมนุษย์ทั้งนั้นแหละ)

เวลาที่เขาออกปากพูดอะไร ลองตั้งใจฟังสิ่งที่เขาสื่อ พวกเราเองถ้ามันไม่สำคัญหรือว่ามันให้ผลลัพท์ที่ดีกว่ามากจริง ๆ เราไม่ค่อยพูดอะไรหรอกครับ คุยกับคนเหนื่อยกว่าสั่งงานคอมพิวเตอร์มาก

ลองเปิดโอกาสให้เขาได้เสนอพัฒนาการที่เกิดจากการสั่งสมทักษะมาอย่างต่อเนื่อง แล้วมันจะเป็นผลประโยชน์ต่อองค์กรของคุณมากมายทีเดียวล่ะ