ทุกบทก่อนหน้านี้สอน skill แบบทั่วไปให้เรา: C, C++, C#, data structures, memory, debugging ไม่มีบทไหนเล็งไปที่ตำแหน่งงานใดตำแหน่งงานหนึ่งโดยเฉพาะเลย ตั้งใจแบบนั้นจริง ๆ แทบไม่มีใครเริ่มอาชีพ game programmer แบบ specialize มาตั้งแต่วันแรกหรอก studio ที่รับ junior programmer เขามองหาความสามารถแก้ปัญหาทั่วไป (general problem-solving) เป็นอันดับแรก ส่วนสายเฉพาะทาง (track) มาทีหลัง
สิ่งที่เกิดขึ้นจริงกับคนส่วนใหญ่คือแบบนี้: ตำแหน่งงานแรกของเรามักกว้าง ๆ อย่าง "Programmer" หรือ "Gameplay Programmer" แล้วในปีสองปีแรกเราจะได้งานหลากหลายแบบปนกันไป — บ้างก็งาน UI บ้างก็ gameplay logic บ้างก็ tool เล็ก ๆ บ้างก็บั๊กที่ซ่อนอยู่ใน network code สักพักในช่วงปีสองปีแรกนั้น จะเริ่มเห็น pattern ว่าเรามักได้รับมอบหมายงานแบบใดแบบหนึ่งซ้ำ ๆ หรือเราอาสาทำงานแบบใดแบบหนึ่งซ้ำ ๆ เอง แล้วนั่นแหละจะค่อย ๆ กลายเป็น track ของเรา ช่วงเวลานี้เรียกว่า specialization fork — จุดที่ "programmer" แตกออกเป็น "gameplay programmer", "graphics programmer" และอื่น ๆ
ข้อยกเว้นคือ graphics, engine และ technical art สามสายนี้พึ่งพา math เชิงลึกหรือความรู้เรื่อง pipeline ที่หยิบมาเรียนรู้เอาระหว่างทำงานแบบสบาย ๆ ได้ยาก studio ที่รับคนเข้าสายพวกนี้โดยเฉพาะเลยมักอยากเห็นฝีมือมาก่อนแล้ว — เช่น shader demo, custom renderer เล็ก ๆ, tool สำหรับ rigging — แม้แต่กับผู้สมัครระดับ junior ถ้าเรารู้ตัวอยู่แล้วว่าอยากไปสายไหนใน 3 สายนี้ การเริ่มสร้าง portfolio ชิ้นเล็ก ๆ ไปในทิศทางนั้นตั้งแต่เนิ่น ๆ ควบคู่ไปกับพื้นฐานกว้าง ๆ จะช่วยได้มาก แทนที่จะรอให้ studio เป็นคนกำหนด specialization ให้เรา
Gameplay programmer คือคนที่เขียนโค้ดที่ทำให้เกมเป็นเกม: เกิดอะไรขึ้นตอนกดปุ่มกระโดด ability ทำงานยังไง quest ติดตามความคืบหน้ายังไง item ใน inventory ถูกใช้ยังไง ถ้า designer อธิบายกฎของเกมออกมาได้ gameplay programmer มักเป็นคนที่แปลงกฎนั้นให้กลายเป็นโค้ดที่ทำงานได้จริง
stand-up ตอนเช้า: เราหยิบ ticket ที่เขียนว่า "เพิ่ม dash ability พร้อม cooldown" ส่วนใหญ่ของวันเราจะอยู่ใน editor ของ Unity หรือ Unreal ไม่ใช่แค่ text editor ธรรมดา — เขียนโค้ดนิดหน่อย กด play เดินตัวละครไปมา ปรับตัวเลข กด play ใหม่อีกรอบ ช่วงบ่ายแก่ ๆ designer เดินมาบอกว่า dash "รู้สึกลอย ๆ" (floaty) เราก็ใช้เวลายี่สิบนาทีปรับ acceleration curve จนรู้สึกโอเค แล้ว commit เปิด pull request แล้ว review โค้ด ability ของคนอื่นก่อนเลิกงาน
งาน gameplay งานแรก ๆ ที่เจอบ่อยมาก: ทำให้การกระโดดรู้สึก "ให้อภัย" มากขึ้น ถ้าเราให้ผู้เล่นกระโดดได้แค่ตอนที่ ground-check บอกว่า "grounded" (แตะพื้นอยู่) เท่านั้น ผู้เล่นที่กดกระโดดหลังเดินตกขอบไปแค่เฟรมเดียวจะไม่ได้อะไรเลย — และมันรู้สึกเหมือนบั๊ก ทั้งที่โค้ดถูกต้องตามหลักการแล้ว ทางแก้คือ coyote time (ตั้งชื่อตามการ์ตูนโคโยตี้ที่จะไม่ตกจนกว่าจะมองลงไปข้างล่าง): ช่วงเวลาผ่อนปรนสั้น ๆ หลังออกจากพื้นที่ยังกดกระโดดได้อยู่
using UnityEngine;
public class JumpController : MonoBehaviour {
public float coyoteTime = 0.15f; // seconds of grace after leaving the ground
public float jumpForce = 8f;
private bool isGrounded;
private float timeSinceGrounded;
private Rigidbody rb;
void Start() {
rb = GetComponent<Rigidbody>();
}
void Update() {
isGrounded = CheckGrounded(); // your own ground-check, not shown here
if (isGrounded) {
timeSinceGrounded = 0f;
} else {
timeSinceGrounded += Time.deltaTime;
}
bool canStillJump = timeSinceGrounded <= coyoteTime;
if (Input.GetButtonDown("Jump") && canStillJump) {
rb.linearVelocity = new Vector3(rb.linearVelocity.x, jumpForce, rb.linearVelocity.z);
timeSinceGrounded = coyoteTime + 1f; // used up, block a second jump this window
}
}
bool CheckGrounded() {
return Physics.Raycast(transform.position, Vector3.down, 1.1f);
}
}
ถ้าไม่มี coyote time จะมีแค่เฟรม 0 เท่านั้นที่กระโดดได้ พอมีแล้ว เฟรม 0 ถึงประมาณเฟรม 4 (จนถึง 0.15 วินาที) ยังกระโดดได้หมด ซึ่งตรงกับสิ่งที่ผู้เล่นคาดหวังจากปฏิกิริยาตอบสนองของตัวเอง ทั้งที่จริง ๆ แล้วเขาออกจากพื้นไปแล้ว
Gameplay programmer มีตำแหน่งงานเปิดรับมากกว่าสายอื่นทุกสายอย่างชัดเจน — แทบทุก studio ทุกขนาดต้องการอย่างน้อยสองสามคน แต่ก็มีคนสมัครต่อตำแหน่งมากที่สุดเช่นกัน เพราะเป็นสายที่ทุกคนนึกถึงเป็นอันดับแรก และเป็น track ที่ portfolio ของ junior ส่วนใหญ่เล็งไปหา เตรียมใจไว้ว่าช่องทางจะแน่นมาก — บางทีการได้สัมภาษณ์ยากกว่าการทำผลงานให้ดีตอนสัมภาษณ์เสียอีก
Graphics programmer (เรียกอีกอย่างว่า rendering programmer) เขียนโค้ดที่กำหนดว่าพิกเซลบนหน้าจอจะออกมาเป็นยังไง: lighting, shadow, reflection, fog, post-processing effect และ GPU (graphics processing unit — ชิปที่วาดเฟรมภาพ) ถูกใช้งานอย่างมีประสิทธิภาพแค่ไหน นี่ไม่ใช่ "ทำให้สวย" แบบทั่ว ๆ ไป แต่เป็นงานเทคนิคขั้นสูงระดับ low-level ใกล้เคียงกับสาย engine/systems มากกว่า gameplay
เราเปิด GPU profiler (เครื่องมืออย่าง RenderDoc หรือ Nsight ที่โชว์ว่าการ์ดจอทำอะไรบ้างในแต่ละเฟรมแบบละเอียด) เพราะด่านหนึ่งรันที่ 40fps แทนที่จะเป็นเป้าหมาย 60fps เราเจอว่าไฟที่ทอดเงาดวงหนึ่งวาด shadow map ใหม่ทั้งอันทุกเฟรม ทั้งที่ควรวาดใหม่แค่ตอนมีอะไรขยับในมุมมองเท่านั้น เราแก้ไข frame time ก็ลดลง แต่ดันมี artifact แบบระยิบระยับที่ผมตัวละครโผล่ขึ้นมาแบบไม่เกี่ยวกัน — บ่ายที่เหลือเลยหมดไปกับเรื่องนั้นแทน
งาน graphics งานแรกที่พบแทบทุกที่คือการทำ diffuse lighting เบื้องต้น (เรียกอีกอย่างว่า Lambertian shading ตามชื่อนักฟิสิกส์ผู้อธิบายมันไว้): พื้นผิวที่หันเข้าหาแสงตรง ๆ ควรดูสว่าง พื้นผิวที่หันหนีแสงควรดูมืด คณิตศาสตร์ที่ใช้คือ dot product หนึ่งครั้งระหว่าง normal ของพื้นผิว (vector ที่ชี้ออกจากพื้นผิวตรง ๆ) กับทิศทางไปหาแสง
// Simplified HLSL-style pixel shader
float3 N = normalize(surfaceNormal); // surface normal, pointing outward
float3 L = normalize(lightDirection); // direction FROM surface TOWARD the light
float brightness = max(dot(N, L), 0.0); // negative would mean "facing away"
float3 finalColor = baseColor * brightness;
dot product ระหว่าง vector ที่ normalize แล้ว (ยาว 1) สอง vector คือค่า cosine ของมุมระหว่างสอง vector นั้นพอดี — 1.0 ตอนชี้ทางเดียวกัน 0.0 ตอนตั้งฉากกัน แล้วติดลบไปเรื่อย ๆ หลังจากนั้น การ clamp ค่าติดลบให้เป็น 0 ด้วย max ป้องกันไม่ให้พื้นผิวที่หันหนีแสงกลายเป็น "สว่างน้อยกว่าศูนย์" ซึ่งไม่มีเหตุผลทางฟิสิกส์เลย
ตำแหน่งงานเปิดรับน้อยที่สุดในบรรดาทุกสายในบทนี้ และยากที่สุดสำหรับ junior ที่จะเข้าไปได้โดยไม่มี demo reel หรือ custom renderer เล็ก ๆ ให้ดู — studio อยากเห็นหลักฐานว่าเราคิดแบบนี้เป็นอยู่แล้ว ไม่ใช่แค่สัญญาว่าจะไปเรียนรู้เอาทีหลัง ข้อดีคือ skill ด้าน graphics ย้ายไปใช้นอกวงการเกมได้เกือบตรง ๆ เลย — studio ภาพยนตร์และ VFX, simulation อุตสาหกรรม, automotive และ robotics visualization, medical imaging, บริษัทชิป/GPU ล้วนอยากได้คนที่เข้าใจ rendering pipeline แบบเดียวกันนี้
Engine programmer (เรียกอีกอย่างว่า systems programmer) สร้างรากฐานระดับ low-level ที่ programmer คนอื่นทุกคนในทีมพึ่งพาโดยไม่ต้องคิดถึงมันเลย: memory allocator, ระบบ entity/component, pipeline การโหลดไฟล์และ asset, ระบบ threading/job ที่กระจายงานไปตาม CPU core ถ้า gameplay programmer สร้างบ้าน engine programmer ก็คือคนเทฐานรากและวางระบบท่อน้ำ
เรากำลังไล่ตาม memory leak ที่โผล่มาให้เห็นก็ต่อเมื่อเล่นต่อเนื่องไปสองชั่วโมงเท่านั้น โดยใช้ heap profiler (เครื่องมือที่ติดตามทุก allocation และที่มาของมัน) เราเจอระบบหนึ่งที่ allocate object เล็ก ๆ ทุกเฟรมแล้วไม่เคย free เลย ทางแก้คือสามบรรทัด อีกเจ็ดชั่วโมงที่เหลือหมดไปกับการหาสามบรรทัดนั้น และเขียน test เพื่อให้แน่ใจว่ามันจะไม่เกิดขึ้นแบบเงียบ ๆ อีก
งาน engine งานแรกแบบคลาสสิก: แทนที่ pattern แบบไร้เดียงสาอย่าง "allocate object ใหม่ทุกครั้ง" ด้วย fixed-size memory pool (บล็อก memory ที่ allocate ไว้ล่วงหน้าและแบ่งเป็นช่องขนาดเท่ากัน นำกลับมาใช้ซ้ำแทนที่จะคืนให้ operating system) นี่คือแนวคิดเรื่องต้นทุนการ allocate แบบเดียวกับในบท memory เอามาใช้กับ action ที่เกิดซ้ำ ๆ จริงในเกมอย่างการยิงกระสุน
#include <iostream>
class BulletPool {
public:
BulletPool(int capacity) : capacity(capacity) {
active = new bool[capacity];
for (int i = 0; i < capacity; i++) active[i] = false;
}
int Allocate() {
for (int i = 0; i < capacity; i++) {
if (!active[i]) {
active[i] = true;
return i; // slot index, not a new heap address
}
}
return -1; // pool full, no allocation happened
}
void Free(int slot) {
active[slot] = false;
}
private:
bool* active;
int capacity;
};
int main() {
BulletPool pool(3);
int a = pool.Allocate();
int b = pool.Allocate();
std::cout << "Allocated slots: " << a << ", " << b << std::endl;
pool.Free(a);
int c = pool.Allocate();
std::cout << "Reused slot: " << c << std::endl;
return 0;
}
ไม่มี new หรือ delete รันเลยหลังจาก setup — ทุก "allocation" คือแค่การพลิกค่า boolean ในอาเรย์ที่ถูกจองไว้ครั้งเดียวเท่านั้น slot 0 ถูกนำกลับมาใช้ทันทีที่ถูก free แทนที่จะขอ memory ใหม่จาก operating system ทุกครั้งที่ยิงกระสุน ที่ 60 นัดต่อวินาที ความต่างตรงนี้คือช่องว่างระหว่าง frame rate ที่ลื่นไหลกับอาการกระตุก
ตำแหน่งงานเปิดรับน้อยกว่า gameplay กระจุกตัวอยู่ที่ studio ใหญ่ ทีมที่โฟกัส engine โดยเฉพาะ และบริษัท middleware turnover ต่ำ — engine programmer ระดับ senior แทบไม่ค่อยลาออก เพราะบทบาทนี้ให้ผลตอบแทนกับความรู้เชิงลึกที่ผูกกับ studio นั้น ๆ ตำแหน่ง junior จึงมีอยู่บ้างแต่ไม่บ่อย และมักอยากได้คนที่แสดงพื้นฐาน C++ ที่แข็งแรงออกมาเองโดยไม่ต้องถาม
Tools programmer สร้างซอฟต์แวร์ที่คนอื่นในทีมใช้ทำงานได้เร็วขึ้น: editor window แบบ custom ใน Unity หรือ Unreal, asset import pipeline, build automation, level editor ที่ทำใช้เองภายในทีม "ผู้ใช้" ของโค้ดที่ tools programmer เขียนแทบไม่ใช่ผู้เล่นเลย แต่เป็น designer, artist หรือ programmer คนอื่นที่นั่งโต๊ะห่างไปสองโต๊ะ
designer บ่นแบบครึ่งจริงครึ่งเล่นว่าการวาง collectible item 200 ชิ้นด้วยมือกินเวลาไปทั้งบ่าย เราใช้เวลาสองชั่วโมงสร้าง editor window เล็ก ๆ: กดปุ่มเดียว มันจะกระจาย item ไปตาม spline (เส้นโค้ง) พร้อมปรับระยะห่างได้ เราส่งมอบให้ designer ใช้ต่อหน้าเราเลย งาน 200 ชิ้นที่เคยกินเวลาทั้งบ่ายเหลือแค่สี่นาที ผลตอบแทนที่เห็นได้ทันทีแบบนี้แหละคือความรู้สึกที่นิยามสายนี้ได้ดีที่สุด
งาน tools งานแรกที่พบบ่อย: เขียน editor script ที่สแกน scene แล้วรายงานปัญหาให้อัตโนมัติ แทนที่จะให้ artist มาเจอเอาโดยบังเอิญตอน playtest ตัวอย่างนี้คือ script ที่หา renderer ทุกตัวที่ไม่มี collider (สาเหตุยอดฮิตของบั๊ก "เดินทะลุกำแพงนี้ได้"):
using UnityEngine;
using UnityEditor;
public class MissingColliderChecker {
[MenuItem("Tools/Check Missing Colliders")]
static void CheckScene() {
Renderer[] renderers = Object.FindObjectsByType<Renderer>(FindObjectsSortMode.None);
int missing = 0;
foreach (Renderer r in renderers) {
if (r.GetComponent<Collider>() == null) {
Debug.LogWarning(r.gameObject.name + " has no Collider!", r.gameObject);
missing++;
}
}
Debug.Log("Check complete. " + missing + " object(s) missing a collider out of " + renderers.Length + " checked.");
}
}
ตัวนี้เพิ่ม menu item ใต้ Tools ใน editor และเปลี่ยนการเช็คแบบ "เดินดูเองแล้วหวังว่าจะสังเกตเห็น" ให้กลายเป็นการสแกนคลิกเดียวจบภายในหนึ่งวินาที ทั้งทีมได้ประโยชน์ทุกครั้งที่ใครก็ตามรัน ไม่ใช่แค่คนที่เขียนมันขึ้นมา
นี่คือหนึ่งใน track ที่เข้าง่ายที่สุดสำหรับ junior จริง ๆ และมันถูกมองข้ามคุณค่าโดยผู้สมัครเองด้วยเหตุผลนี้แหละ — "tools programmer" ฟังดูน่าตื่นเต้นน้อยกว่า "gameplay programmer" บนกระดาษ คนสมัครเลยน้อยกว่า ทั้งที่ skill เบื้องหลังทับซ้อนกับ gameplay เยอะมาก tool ที่ทำเสร็จแล้วยังเป็นหนึ่งใน portfolio piece ที่ demo ได้ชัดเจนที่สุดที่ junior คนหนึ่งจะมีได้: มันเล็ก มันทำงานได้จริงเห็นชัด ๆ และ hiring manager เห็นคุณค่าได้ภายในสิบวินาที
Technical artist (มักเรียกสั้น ๆ ว่า "tech artist") อยู่ตรงกลางระหว่างทีม art กับทีม programming งานของเขาครอบคลุมทุกอย่างที่ตกอยู่ในช่องว่างระหว่างสองฝั่งนี้: เขียน shader ให้ artist ใช้ได้โดยไม่ต้องเขียนโค้ดเอง สร้างและแก้ character rig ตั้งค่า animation pipeline และหาสาเหตุว่าทำไม visual effect ที่สวยงามชิ้นหนึ่งถึงแอบกิน frame time ไป 8 มิลลิวินาที
เช้า: อยู่ใน Blender หรือ Maya แก้ character rig ที่ไหล่บีบตัวตอนหมุนสุดมุม (ปัญหา candy-wrapper จากบท rigging) บ่าย: เขียน shader เล็ก ๆ ให้ artist คุม effect "dissolve" ได้ด้วย slider ตัวเดียว แทนที่จะต้องแก้โค้ด shader เอง stand-up ตอนเย็น: อธิบายให้ art director ฟังด้วยภาษาง่าย ๆ ว่าทำไม effect ที่ขอมาถึงกิน frame time เกินไปบน hardware เป้าหมาย แล้วเสนอทางเลือกที่ถูกกว่าแต่ยังดูใกล้เคียงพอ
งาน tech-art งานแรกที่พบบ่อย คือเขียน pipeline script เล็ก ๆ ที่เช็คว่า asset ทำตามข้อกำหนดการตั้งชื่อของทีมก่อนถูก import — จับข้อผิดพลาดได้ภายในไม่กี่วินาที แทนที่จะให้ programmer มาเจอทีหลังในรูปแบบของ broken reference ที่งงงวย
# Python, run inside Maya (using Maya's "cmds" module to talk to the scene)
import maya.cmds as cmds
REQUIRED_PREFIXES = {"joint": "jnt_", "mesh": "geo_"}
def check_naming():
problems = []
for obj in cmds.ls(dag=True, long=False):
obj_type = cmds.objectType(obj)
prefix = REQUIRED_PREFIXES.get(obj_type)
if prefix and not obj.startswith(prefix):
problems.append(obj)
if problems:
print("Naming problems found:")
for p in problems:
print(" " + p)
else:
print("All names follow convention.")
check_naming()
มี object สองตัวที่ผิดข้อกำหนด: Shoulder_L ควรเป็น jnt_Shoulder_L และ Rock_Big ควรเป็น geo_Rock_Big script แบบนี้รันเสร็จภายในไม่กี่วินาทีและจับข้อผิดพลาดเล็ก ๆ ที่มองข้ามง่ายแบบนี้ได้พอดี ซึ่งถ้าไม่จับตอนนี้มันจะกลายเป็นบั๊กที่งงงวยในภายหลัง ตอนที่ขั้นตอน export หรือ import สมมติไปแล้วว่าทุกอย่างตั้งชื่อตามข้อกำหนด
ความต้องการสูงเมื่อเทียบกับจำนวนคนที่มีคุณสมบัติจริง ๆ คอขวดไม่ใช่ตำแหน่งงานที่เปิดรับ — แต่คือการหาคนที่เก่งจริงทั้งสองด้านของงาน เพราะ programmer ส่วนใหญ่ที่แตะ ๆ งาน art หรือ artist ที่แตะ ๆ งาน code มักอยู่ฝั่งใดฝั่งหนึ่งอย่างมั่นคง ถ้าเราทำได้ทั้งสองด้านจริง ๆ แม้แค่ระดับ junior เราก็หายากกว่าที่คิดไว้มาก
AI programmer เขียนโค้ดตัดสินใจให้ non-player character (NPC) — ศัตรู เพื่อนร่วมทาง ฝูงชน ทั้งที่ชื่อสายนี้มีคำว่า AI แต่แทบไม่ใช่ machine learning แบบ "เทรน neural network" ในความหมายสมัยใหม่เลย ส่วนใหญ่เป็น finite state machine (ระบบที่อยู่ในสถานะที่มีชื่อเรียกได้เพียงหนึ่งสถานะเสมอ อย่าง Idle หรือ Chase แล้วมีกฎกำหนดการสลับสถานะ), behavior tree (tree ของเงื่อนไขกับ action ที่ใช้เลือกพฤติกรรม) และ pathfinding
ระหว่าง playtest ศัตรูตัวหนึ่งยืนนิ่งอยู่กับที่ทั้งที่ผู้เล่นยิงมันจากระยะที่มองเห็นชัดเจน เราเจาะลึกเข้าไปใน behavior tree แล้วเจอเงื่อนไข vision-check ที่ทำงานแค่วินาทีละครั้งแทนที่จะเป็นทุกเฟรม — บั๊กเรื่อง timing ที่หายากมากและโผล่มาเฉพาะตอนผู้เล่นเข้าใกล้จากมุมใดมุมหนึ่งเท่านั้น เราแก้เงื่อนไขนั้น แล้วใช้เวลาที่เหลือของวันปรับ aggro range กับ reaction delay ร่วมกับ designer ที่พูดซ้ำ ๆ ว่า "ดุขึ้นอีกนิด" จนกว่าจะรู้สึกโอเค
งาน AI งานแรกที่พบแทบทุกที่: ศัตรูสามสถานะพื้นฐาน — Idle, Chase, Attack — สลับสถานะตามระยะห่างจากผู้เล่น
using UnityEngine;
public class EnemyAI : MonoBehaviour {
enum State { Idle, Chase, Attack }
public Transform player;
public float chaseRange = 8f;
public float attackRange = 2f;
State currentState = State.Idle;
void Update() {
float distance = Vector3.Distance(transform.position, player.position);
State newState = currentState;
if (distance <= attackRange) {
newState = State.Attack;
} else if (distance <= chaseRange) {
newState = State.Chase;
} else {
newState = State.Idle;
}
if (newState != currentState) {
Debug.Log("State changed: " + currentState + " -> " + newState + " (distance " + distance.ToString("F1") + ")");
currentState = newState;
}
}
}
state จะ log การเปลี่ยนแปลงก็ต่อเมื่อมันสลับจริง ๆ เท่านั้น ไม่ใช่ทุกเฟรม — รายละเอียดเล็ก ๆ แต่สำคัญ เพราะระบบ AI ที่ตอบสนองทุกเฟรมไม่ว่าจะมีการเปลี่ยนแปลงหรือไม่จะกลายเป็นระบบที่ noisy และเปลืองงานคำนวณอย่างรวดเร็ว ทุกอย่างต่อจากนี้ — การเปลี่ยนสถานะที่ฉลาดขึ้น การหลบเข้าที่กำบัง การประสานงานเป็นกลุ่ม — ล้วนสร้างต่อยอดจากโครงเดียวกันนี้: "เช็คเงื่อนไข แล้วสลับ state"
เล็กกว่า gameplay โดยรวม studio ขนาดเล็กและกลางจำนวนมากไม่มีตำแหน่ง "AI programmer" แยกต่างหากเลย — งานนี้ถูกรวมเข้าไปอยู่ในบทบาท gameplay programmer ทั่วไป ตำแหน่ง AI programmer แบบเฉพาะทางจะกระจุกอยู่ที่ studio ใหญ่ที่มีระบบ NPC ซับซ้อน (เกม open-world, เกมแนว tactics/strategy, ศัตรูจำนวนมากหลายแบบ) ที่ซึ่งโค้ด AI ใหญ่พอจนต้องมีคนดูแลเฉพาะทาง
Network programmer ทำให้ multiplayer ทำงานได้: รักษาความ sync ของเกมผู้เล่นหลายคน ตัดสินว่าเครื่องคอมพิวเตอร์ไหน "ได้รับอนุญาต" ให้ถูกต้อง ตอนที่ผู้เล่นสองคนขัดแย้งกัน และสร้าง RPC call (remote procedure call — การเรียกฟังก์ชันที่โค้ดรันอยู่บนเครื่องคนละเครื่อง) ที่ทำให้ action ของผู้เล่นคนหนึ่งไปโผล่ถูกต้องบนหน้าจอของคนอื่นทุกคน network programmer หลายคนยังดูแล backend ด้วย — server ที่อยู่นอกตัวเกม client เอง: account, matchmaking, inventory, leaderboard, live event
tester รายงานว่าบน connection ที่แย่ ตัวละครของผู้เล่นคนอื่น "rubber-band" — กระตุกถอยหลังกะทันหัน เราสืบไปเจอว่า client-side prediction (client เดาตำแหน่งตัวละครไว้ล่วงหน้าก่อนที่ server จะยืนยัน) ขัดแย้งกับตำแหน่งที่ server ยืนยันแล้วมากเกินไป เราใช้เวลาทั้งวันปรับวิธี smooth การแก้ไขตำแหน่ง แล้วสลับไปเขียน backend endpoint ที่ให้ reward สำหรับ login รายวัน แล้วปิดวันด้วยการ load-test ด้วย script ที่จำลอง login ปลอม 500 ครั้งพร้อมกัน
งาน networking งานแรกที่พบบ่อยเมื่อใช้ framework อย่าง Netcode for GameObjects ของ Unity: ทำให้ตำแหน่งของผู้เล่นคนหนึ่งอัปเดตถูกต้องบนหน้าจอของผู้เล่นคนอื่นทุกคน โดยให้ server เป็นฝ่าย authority (แหล่งความจริงเพียงหนึ่งเดียว เพื่อไม่ให้ผู้เล่น teleport ได้เองแค่แก้ตำแหน่ง local ของตัวเอง)
using Unity.Netcode;
using UnityEngine;
public class NetworkedPlayer : NetworkBehaviour {
// NetworkVariable: a value the server owns, and Netcode
// automatically replicates to every connected client
NetworkVariable<Vector3> networkPosition = new NetworkVariable<Vector3>();
void Update() {
if (IsOwner) {
// Only the player who owns this character asks the server to move it
Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
RequestMoveServerRpc(transform.position + input * Time.deltaTime * 5f);
}
// Every client, including the owner, renders from the replicated value
transform.position = networkPosition.Value;
}
[ServerRpc]
void RequestMoveServerRpc(Vector3 requestedPosition) {
// The server decides the real position (could reject/clamp it here)
networkPosition.Value = requestedPosition;
}
}
Client A ไม่เคยตั้งตำแหน่งของตัวเองโดยตรงเลย — มันแค่ขอเท่านั้น server เป็นเครื่องเดียวที่เขียนค่าลงไปใน networkPosition จริง ๆ การไหลของข้อมูลแบบทิศทางเดียวนี้ (client ขอ, server ตัดสิน, ทุกคน render จากคำตอบของ server) คือแนวคิดหลักที่เรื่อง networking ที่เหลือทั้งหมดสร้างต่อยอดขึ้นไป รวมถึงปัญหาที่ยากกว่านี้อย่างการจัดการผู้เล่นที่ขอ move แบบที่เป็นไปไม่ได้
ตำแหน่งงานเปิดรับน้อยกว่า gameplay แต่มั่นคง — เกม multiplayer หรือ live-service ทุกเกมต้องมี network programmer อย่างน้อยหนึ่งคน และประเภทเกมแบบนี้ก็ไม่ได้หดตัวลง เข้ายากจริง ๆ สำหรับ junior: บั๊กเรื่อง network มักไม่ deterministic (เกิดซ้ำแบบเดิมไม่ได้ทุกครั้ง) และต้องใช้ประสบการณ์ในการคิดวิเคราะห์อย่างมีประสิทธิภาพ track นี้จึงพึ่งพา mentorship อย่างหนักในช่วงแรก
UI programmer สร้างเมนู, HUD (heads-up display — overlay บนหน้าจอที่โชว์เลือดม, กระสุน, minimap และอื่น ๆ คล้ายกัน), หน้าจอ inventory และเมนู settings แล้วเขียน code layer ที่เชื่อม visual เหล่านั้นเข้ากับข้อมูลและ event จริงของเกมที่อยู่เบื้องหลัง
เราเชื่อม shield bar ตัวใหม่ให้เติมและลดค่าถูกต้องควบคู่ไปกับ health bar ที่มีอยู่แล้ว QA รายงานว่า shield bar ทับกับ health bar บนจอ ultra-wide — บั๊กเรื่อง anchoring ที่โผล่มาเฉพาะบาง aspect ratio เท่านั้น เราแก้ layout แล้วใช้เวลาหนึ่งชั่วโมงตรวจสอบให้ controller เดิน grid ของ inventory ได้เหมือนกับที่ mouse ทำได้ เพราะทั้งสองต้องทำงานให้เหมือนกันเป๊ะ
งาน UI งานแรกที่พบบ่อย: ทำให้ health bar ตอบสนองต่อ damage event แทนที่จะถูกเช็คและวาดใหม่ทุกเฟรม (ซึ่งเปลืองงานคำนวณและ sync ยากกว่า)
using UnityEngine;
using UnityEngine.UI;
public class HealthBarUI : MonoBehaviour {
public Image fillImage; // the bar's fill graphic, Image Type = Filled
public PlayerHealth health; // has an event: OnDamage(int currentHp, int maxHp)
void OnEnable() {
health.OnDamage += HandleDamage;
}
void OnDisable() {
health.OnDamage -= HandleDamage;
}
void HandleDamage(int currentHp, int maxHp) {
float fraction = (float)currentHp / maxHp;
fillImage.fillAmount = fraction;
Debug.Log("Health bar updated: " + currentHp + "/" + maxHp + " (" + (fraction * 100f).ToString("F0") + "%)");
}
}
bar ทำงานก็ต่อเมื่อ OnDamage เกิดขึ้นจริงเท่านั้น subscribe และ unsubscribe อย่างเป็นระเบียบใน OnEnable/OnDisable เพื่อไม่ให้มันแอบฟังต่อหลัง object หายไปแล้ว pattern แบบ event-driven นี้ — ตอบสนองต่อการเปลี่ยนแปลง ไม่ต้อง poll เช็คทุกเฟรม — คือแกนหลักของงาน UI programming แทบทั้งหมด
ความต้องการมั่นคง — ทุกเกมต้องมี UI งานนี้เลยไม่มีวันหายไป แต่ที่ studio เล็ก ๆ มักถูกรวมเข้าไปในงาน gameplay ทั่วไปแทนที่จะเป็นตำแหน่งแยกต่างหาก ในที่ที่มีตำแหน่ง UI programmer แยกจริง ๆ มักแน่นน้อยกว่าตำแหน่ง "gameplay programmer" ทั่วไป เพียงเพราะมันฟังดูน่าตื่นเต้นน้อยกว่าและผู้สมัคร junior เล็งไปที่มันโดยตรงน้อยกว่า
Audio programmer เชื่อม sound effect และเพลงเข้ากับ event ของเกม สร้าง logic การ mix (ตัดสินว่าเสียงต่าง ๆ ควรดังแค่ไหนเทียบกัน และเมื่อไหร่) และ integrate audio middleware (toolkit จากบุคคลที่สาม — Wwise กับ FMOD เป็นสองตัวที่ใช้กันมากที่สุด — ที่ sound designer ใช้สร้าง interactive audio ได้โดยไม่ต้องพึ่ง programmer ทุกครั้งที่มีการเปลี่ยนแปลง)
sound designer รายงานว่าเสียงฝีเท้าดังสองครั้งบนพื้นคอนกรีตแต่ไม่เคยเกิดบนพื้นหญ้าเลย เราสืบไปเจอว่า animation event ทำงานสองครั้งต่อฝีเท้าใน animation clip หนึ่ง แต่ทำงานแค่ครั้งเดียวในอีก clip หนึ่ง — บั๊กเรื่อง animation ที่แต่งตัวมาเป็นบั๊กเรื่อง audio บ่าย: implement music ducking (ลดเสียงเพลงอัตโนมัติตอนตัวละครกำลังพูด) เพื่อให้บทพูดยังฟังชัดโดยไม่ต้องให้ sound designer มาคอยปรับ volume fader เองทุกบรรทัด
งาน audio งานแรกที่พบบ่อย: implement music ducking ให้เพลงพื้นหลังเบาลงอัตโนมัติตอนบทพูดกำลังเล่นอยู่ แล้วกลับมาดังเหมือนเดิมตอนบทพูดจบ
using UnityEngine;
public class MusicDucker : MonoBehaviour {
public AudioSource music;
public AudioSource dialogue;
public float duckedVolume = 0.25f;
public float normalVolume = 1.0f;
public float duckSpeed = 2f; // volume change per second
void Update() {
float target = dialogue.isPlaying ? duckedVolume : normalVolume;
music.volume = Mathf.MoveTowards(music.volume, target, duckSpeed * Time.deltaTime);
}
}
Mathf.MoveTowards เลื่อนค่า volume ไปหา target ที่ถูกต้อง ณ ตอนนั้นอย่างนุ่มนวล แทนที่จะกระโดดไปทันที — การกระโดดของ volume แบบทันทีทันใดสังเกตเห็นได้ชัดมากและฟังดูเหมือนความผิดพลาด ในขณะที่การเลื่อนแบบนุ่มนวลครึ่งวินาทีให้ความรู้สึกเหมือนการ mix ที่ตั้งใจทำ
จำนวนตำแหน่งเฉพาะทางน้อยที่สุดในบรรดาทุก track ในบทนี้ studio จำนวนมากไม่มี audio programmer เฉพาะทางเลย — sound designer จัดการ scripting ง่าย ๆ เองบ้าง หรืองานถูกรวมเข้าไปใน gameplay/tools programming ตำแหน่งเฉพาะทางที่มีอยู่จริงกระจุกตัวอยู่ที่ studio ใหญ่และบริษัท middleware เอง (Audiokinetic, Firelight) และแข่งขันสูงเพราะมีที่นั่งน้อยมากพอดี
เมื่อวางเทียบกัน ทั้งเก้า track แยกออกจากกันตามแกนที่ค่อนข้างคงที่อยู่ไม่กี่แกน: งานประจำวันพึ่งพา math มากแค่ไหน ภาษาไหนครองสาย ประมาณว่ามีตำแหน่งงานเปิดรับเยอะแค่ไหน และปกติแล้วยากแค่ไหนที่จะเข้าไปในฐานะ junior ตัวเลขเหล่านี้ไม่ใช่ค่าที่แม่นยำเป๊ะ ๆ — มันแตกต่างกันไปตาม studio และภูมิภาค — แต่ลำดับเปรียบเทียบโดยรวมนี้ยังคงเป็นจริงสม่ำเสมอทั่วทั้งวงการ
ตัวเลขข้างบนซ่อนบางอย่างที่สำคัญกว่าจำนวนดิบ ๆ เอาไว้:
อ่านคำอธิบายอย่างเดียวพาเราไปได้แค่ระดับหนึ่ง — แทบทุก track ฟังดูน่าสนใจในหนึ่งย่อหน้าทั้งนั้น การทดสอบจริง ๆ คือการสร้างของเล็ก ๆ ในแต่ละ track แล้วสังเกตสัญญาณที่เฉพาะเจาะจงมาก: ไม่ใช่ "ทำเสร็จไหม" แต่คือ ยังคิดถึงมันอยู่หรือเปล่าหลังจากปิดโน้ตบุ๊กไปแล้ว สัญญาณตัวที่สองนี้ซื่อสัตย์กว่าความสนุกที่รู้สึกระหว่างสองชั่วโมงที่ใช้ทำมันมาก เพราะมีงานหลายอย่างที่สนุกตอนทำแต่ลืมได้ทันที และมีบางอย่างที่น่ารำคาญนิด ๆ ตอนทำแต่ดันดึงความสนใจกลับมาตอนเย็นวันนั้นอยู่ดี
แต่ละอันนี้เล็กพอจะลองทำในหนึ่งสุดสัปดาห์ได้ โดยใช้แค่สิ่งที่บทก่อน ๆ สอนไปแล้ว บวกกับ engine API เฉพาะที่ต้องไปค้นหาเพิ่มระหว่างทำ:
หลังลองทำสักสองสามอย่างในเก้าอันนี้ คำถามพวกนี้มักช่วยจัดกลุ่มคนได้เร็ว ความซื่อสัตย์สำคัญกว่าการตอบ "ใช่" กับข้อที่ฟังดูน่าประทับใจ:
คนส่วนใหญ่ติ๊กถูกกระจายอยู่สองสาม track ไม่ใช่แค่ track เดียว — การซ้อนทับกันแบบนี้เป็นเรื่องปกติและเป็นข้อมูลที่มีประโยชน์ ไม่ใช่ความล้มเหลวในการหาคำตอบเดียวที่ "ถูกต้อง"
ชื่อ track เดียวกันอาจหมายถึงงานที่ต่างกันชัดเจนขึ้นอยู่กับว่าเราไปอยู่ studio แบบไหน สามรูปแบบกว้าง ๆ นี้ครอบคลุมวงการส่วนใหญ่:
studio แบบ mobile live-service (แนวเดียวกับที่เกมสไตล์ HoYoverse ถูกสร้างขึ้นมา) พึ่งพา tools กับ network/backend programmer อย่างหนัก เพราะการปล่อย event ใหม่ทุกไม่กี่สัปดาห์จะทำได้ก็ต่อเมื่อคนที่ไม่ใช่ programmer ประกอบมันเองได้ผ่าน tool ที่ดี และเพราะระบบเศรษฐกิจ, account, event ของเกม live ทั้งหมดวิ่งผ่าน backend system ภาระงาน UI programming ก็หนักเช่นกัน — banner, หน้าจอ event, เมนูหมุนเวียนตลอดเวลา
studio แบบ console/PC AAA พึ่งพา graphics กับ engine programmer อย่างหนัก เพราะความสวยงามทาง visual ตลอด production ที่ยาวหลายปีมักเป็นตัวสินค้าจริง ๆ ที่ขายอยู่ และทีมมักใหญ่พอที่จะแยก track ออกจากกันอย่างชัดเจน — การย้าย track ในบริษัทเดียวกันอาจใช้เวลานานกว่าที่ studio เล็กกว่า
studio แบบ indie แทบไม่มี track แยกกันเลย ทีมที่มี programmer สามสี่คนหมายความว่าทุกคนทำทั้ง gameplay นิดหน่อย tools นิดหน่อย บางทีก็ graphics นิดหน่อย ด้วยความจำเป็นไม่ใช่ทางเลือก การ specialize ทำได้น้อยกว่า แต่ข้อดีคือความเป็นเจ้าของงานที่กว้างขึ้นและ feedback ที่เร็วและตรงมากขึ้นจากงานของตัวเอง
พื้นฐานกว้าง ๆ จากบทก่อน ๆ นี่แหละที่ทำให้การย้ายแบบนั้นเป็นไปได้ในภายหลัง skill เรื่อง C++, C# และ data structures ที่อยู่เบื้องหลังทุก track ในบทนี้คือ skill ชุดเดียวกัน — แค่เล็งไปคนละทิศทางขึ้นอยู่กับว่าปีนี้เราอยู่ track ไหน
"We are looking for a Programmer with a strong grasp of 3D math,
experience with shader authoring, and familiarity with GPU profiling
tools. You will optimize our lighting pipeline and reduce frame time
on target hardware."
ประกาศงานนี้เป็นของ track ไหนในบทนี้? เขียนสองประโยคข้อกำหนดใหม่ด้วยภาษาง่าย ๆ ที่ผู้เริ่มต้นเข้าใจได้
นี่คือประกาศงานของ graphics/rendering programmer มีเบาะแสสามอย่างที่ชี้ตรงไปที่มัน: "3D math" (linear algebra), "shader authoring" (เขียนโค้ด HLSL/GLSL) และ "GPU profiling tools" (เครื่องมือสไตล์ RenderDoc/Nsight) ซึ่งทั้งหมดถูกระบุตรง ๆ ในรายการ "What You Must Be Strong At" ของหัวข้อ 3
เขียนใหม่ด้วยภาษาง่าย ๆ: "เราต้องคุ้นเคยกับ vector และ matrix และเขียนโปรแกรมเล็ก ๆ (shader) ที่รันบนการ์ดจอได้ เรารู้วิธีใช้เครื่องมือที่โชว์ว่าแต่ละส่วนของเฟรมใช้เวลาบน GPU นานแค่ไหน เพื่อให้เกมรันเร็วขึ้นได้โดยไม่ทำให้ภาพแย่ลง"
เหมาะที่สุด: Engine/Systems และ Tools Engine/Systems สร้างขึ้นบนเนื้อหาเรื่อง memory กับ pointer โดยตรง และให้ผลตอบแทนกับความถูกต้องแบบ deterministic ที่วัดผลได้ แทนที่จะเป็นการปรับจูน "ความรู้สึก" แบบอัตวิสัย — ตรงกับสิ่งที่ผู้สมัครบอกว่าชอบและต้องการพอดี Tools ก็ให้ผลตอบแทนกับความถูกต้องแบบ "ทำงานได้หรือไม่ได้" ที่ชัดเจนเช่นกัน มี feedback เรื่องความรู้สึกของผู้เล่นที่คลุมเครือแบบที่ gameplay programmer ต้องเจอทุกวันน้อยกว่ามาก และไม่ต้องการสายตาทางศิลปะที่ผ่านการฝึกมา
เหมาะน้อยที่สุด: Technical Artist หัวข้อ 6 ระบุ "สายตาที่ผ่านการฝึกฝนมาจริง ๆ สำหรับคุณภาพงาน visual" เป็นข้อกำหนดที่ไม่ใช่ตัวเลือกเสริม ซึ่งผู้สมัครบอกตรง ๆ ว่าตัวเองไม่มี และงานส่วนใหญ่ถูกตัดสินด้วย feedback แบบอัตวิสัย "มันดูถูกต้องไหม" ซึ่งใกล้เคียงกับ feedback ที่คลุมเครือแบบที่ผู้สมัครบอกว่าทำให้หงุดหงิด
// Simplified Netcode-style server RPC
[ServerRpc]
void RequestFireServerRpc(Vector3 origin, Vector3 direction) {
if (Physics.Raycast(origin, direction, out RaycastHit hit, 100f)) {
ApplyDamageClientRpc(hit.collider.gameObject.GetComponent<NetworkObject>().NetworkObjectId, 10);
}
}
บอกชื่อ track แล้วให้เหตุผลสองข้อที่ดึงมาจากรายการ "What You Must Be Strong At" ของ track นั้นในบทนี้โค้ดนี้เป็นของ network/backend programmer เหตุผลสองข้อ ทั้งคู่มาจากหัวข้อ 8 โดยตรง:
1. มันถูกเขียนเป็น [ServerRpc] — โค้ดที่รันบน server เท่านั้นเสมอ ตรงกับ pattern "server เป็น authority" จากงาน junior task ในหัวข้อ 8 ที่ server เป็นฝ่ายตัดสินผลลัพธ์จริง ส่วน client แค่ขอเท่านั้น
2. การตัดสินว่ากระสุนนัดหนึ่งโดนเป้าจริงไหม แล้วค่อยบอกทุก client ให้ apply damage ทีหลัง ต้องใช้ "การคิดอย่างระมัดระวังเรื่อง state กับ timing" ตรงตามข้อในรายการ "What You Must Be Strong At" ของหัวข้อ 8 เป๊ะ — server ต้องเป็นแหล่งความจริงเพียงหนึ่งเดียว ไม่งั้นผู้เล่นสองคนอาจเห็นผลลัพธ์ของกระสุนนัดเดียวกันไม่ตรงกันได้
(gameplay programmer อาจเป็นคนเขียนตัวเลข damage ดิบ ๆ หรือ logic ของ raycast ในบริบทเกมผู้เล่นคนเดียวได้ แต่ทันทีที่มันถูกห่อด้วยคู่ ServerRpc/ClientRpc ความเป็นเจ้าของก็ตกอยู่กับใครก็ตามที่รับผิดชอบรักษาให้เกมของผู้เล่นทุกคนตรงกัน)