บทความฉบับเร่งรัดสำหรับผู้ต้องการสร้างเกม 3D แนว Action มุมมองบุคคลที่ 3 ด้วย Unity ครับ สำหรับผู้ที่ต้องการไอเดียการพัฒนาเกม
อันที่จริงนี่เป็นหนึ่งในเครื่องมือประกอบรายละเอียดงานวิจัยในห้องเรียน มหาวิทยาลัยธุรกิจบัณฑิตย์ คณะเทคโนโลยีสารสนเทศ สาขาวิชาออกแบบเชิงโต้ตอบ และพัฒนาเกมที่ผมกำลังทำอยู่เกี่ยวกับการเรียนภาษาโปรแกรม กับการพัฒนาตัว และทัศนคติของผู้เรียนการเขียนโปรแกรมที่มีต่อเครื่องมือ Game Creator Template ที่ทางผมพัฒนาขึ้น เพื่อศึกษาการเขียนโปรแกรมแบบ Backward (สนใจสอบถาม หรือรีวิวรายละเอียดเรื่องงานวิจัยทักได้ที่ Fan Page ครับ)
เบื้องต้นนั้นผมได้ทำ Package ของ Unity ออกมาให้แล้วสำหรับคนที่ต้องการใช้เป็นตัวอย่างในการพัฒนาเกม 3 มิติ โดย Asset ที่ผมใช้มีทั้งตัวจ่ายเงิน คือ Animal Adventrue Pack (10$) และ Cartoon City (5$) ส่วนนี้ขอความกรุณานำไปใช้เพื่อการศึกษาอย่าได้นำไปใช้เชิงพาณิชย์ เลยนะครับ
ตัว Package นั้นผมได้อัพโหลดไว้ที่
http://bit.ly/DAYDEVKIT
[เป็นเวอร์ชัน 1 ที่ยังไม่ได้ใส่ส่วนของ เสียงไว้ให้ครับ รองรับ Unity version 5 ขึ้นไปเท่านั้นนะครับ]
วิธีการนำ Package Daydev Action Kit ไปใช้
ขั้นตอนแรกให้ คลาย zip ออกมาเป็น package ของ Unity ครับ
เสร็จแล้วเปิดโปรแกรม Unity ขึ้นมา (เวอร์ชัน 5) เท่านั้น สร้าง Project ใหม่ขึ้นมาครับ
ไปที่เมนู Asset -> Import Package -> Custom Package…
เลือกไฟล์ Daydev Action Kit ที่เป็น Package ที่เราโหลดมาใส่เข้าไป รอสักพักครับ ก็เป็นอันเรียบร้อย
ลองตรวจสอบเราจะเห็นว่า โฟลเดอร์ Prefabs นั้นจะมี สิ่งพร้อมใช้ให้ลากวางก็กลายเป็นเกมๆ หนึ่งได้เลยครับ
ใน โฟลเดอร์ Scene ก็จะมี ด่านตัวอย่างที่ทำไว้ให้เรียบร้อย วิธีการก็คือ ลาก Prefabs ไปวางทีละส่วนๆ นั่นแหละครับ แต่ผมทำไว้ให้แล้ว
เปิด Scene ที่ชื่อ Example01 ได้เลยครับ
ตัวละครจะพร้อมใช้งาน หรือพร้อมเล่นเลย
ส่วนประกอบต่างๆ และการทำงาน
Player ของตัวละครจะใช้ Code player.cs ครับ ซึ่งจะมี Code ดังนี้
using UnityEngine; using System.Collections; public class Player : MonoBehaviour { Animator anim; public float speed = 6.0F; public float jumpSpeed = 12.0F; public float gravity = 13.0F; private Vector3 moveDirection = Vector3.zero; public bool Running = false; public bool Jumping = false; public bool Death = false; public ParticleEmitter waterDeath; public ParticleEmitter deathLight; public ParticleEmitter AttackEffect; public float fireRate = 0.3f; private float nextFire = 0.0f; //Mouse public float rotationSpeed = 100.0F; public float horizontalSpeed = 2.0F; public float verticalSpeed = 2.0F; //Health public GUISkin HealthBarSkin; public int healthBar = 500; public bool isWinner = false; public bool isGameOver = false; void Start(){ Cursor.visible = false; anim = GetComponent <Animator> (); Time.timeScale = 1; } public void Winning() { Debug.Log("Winn!!!!!!"); isWinner = true; } void Update() { CharacterController controller = GetComponent<CharacterController>(); if (controller.isGrounded) { moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); moveDirection = transform.TransformDirection(moveDirection); moveDirection *= speed; if (Input.GetButton("Jump")){ moveDirection.y = jumpSpeed; Jumping = true; }else{ Jumping = false; } if (Input.GetKey((KeyCode.D)) || Input.GetKey("right") || Input.GetKey((KeyCode.A)) || Input.GetKey("left") || Input.GetKey((KeyCode.D)) || Input.GetKey("up") || Input.GetKey((KeyCode.W)) || Input.GetKey("down") || Input.GetKey((KeyCode.S)) ){ Running = true; }else{ Running = false; } if(Running == true){ //anim.SetTrigger("Running"); anim.SetBool ("IsWalking", true); }else{ anim.SetBool ("IsWalking", false); } if(Jumping == true){ //anim.SetTrigger("Running"); anim.SetBool ("IsJumping", true); }else{ anim.SetBool ("IsJumping", false); } } float h = horizontalSpeed * Input.GetAxis("Mouse X"); transform.Rotate(0, h, 0); //Attack if (Input.GetMouseButtonDown (0)&& Time.time > nextFire) { nextFire = Time.time + fireRate; anim.SetBool ("IsAttack", true); //Debug.Log ("Attack"); } else { anim.SetBool ("IsAttack", false); } //Mouse movement float translation = Input.GetAxis("Vertical") * speed; float rotation = Input.GetAxis("Horizontal") * rotationSpeed; translation *= Time.deltaTime; rotation *= Time.deltaTime; transform.Translate(0, 0, translation); transform.Rotate(0, rotation, 0); moveDirection.y -= gravity * Time.deltaTime; controller.Move(moveDirection * Time.deltaTime); } void OnTriggerEnter(Collider theCollision){ if (theCollision.gameObject.name == "Sea") { Death = true; if (Death == true) { anim.SetTrigger ("Death"); Instantiate(waterDeath, transform.position, transform.rotation); //Destroy(gameObject); StartCoroutine(WaitDeath()); } //Application.LoadLevel ("Stage1"); } else { Death = false; } if (theCollision.gameObject.name == "Monsters") { healthBar-=20; if(healthBar <=0){ healthBar = 0; anim.SetTrigger ("Death"); StartCoroutine(WaitDeath()); } } if (theCollision.gameObject.name == "hits") { healthBar-=10; if(healthBar <=0){ healthBar = 0; anim.SetTrigger ("Death"); StartCoroutine(WaitDeath()); } } if (theCollision.gameObject.name == "hitsboss") { healthBar-=140; if(healthBar <=0){ healthBar = 0; anim.SetTrigger ("Death"); StartCoroutine(WaitDeath()); } } if (theCollision.gameObject.name == "BossBullet(Clone)") { Instantiate(AttackEffect, transform.position, transform.rotation); healthBar-=70; if(healthBar <=0){ healthBar = 0; anim.SetTrigger ("Death"); StartCoroutine(WaitDeath()); } } if (theCollision.gameObject.name == "WinCondition") { Winning (); } } IEnumerator WaitDeath() { Instantiate(deathLight, transform.position, transform.rotation); yield return new WaitForSeconds(2); isGameOver = true; } void OnGUI(){ if (isWinner == true) { Time.timeScale = 0; //Show Total Score GUI.Box(new Rect(Screen.width/4, Screen.height/4+10, Screen.width/2, Screen.height/2), "Win!!!!: "); //Restart Game if (GUI.Button(new Rect(Screen.width/4+10, Screen.height/4+Screen.height/10+50, Screen.width/2-20, Screen.height/10), "Next Stage")){ Application.LoadLevel(Application.loadedLevel); } } //int IntScore = score; if(!isGameOver){ GUI.skin = HealthBarSkin; GUI.Label(new Rect(Screen.width-(Screen.width-35), 32, Screen.width/6, Screen.height/6), "HP: "/*+IntScore.ToString()*/); GUI.Label(new Rect(Screen.width/2, Screen.height/2-25, 100, 50),"+"); GUI.backgroundColor = Color.yellow; GUI.Button(new Rect(Screen.width-(Screen.width-35),12,(healthBar/2),20),""); }else{ Time.timeScale = 0; //Show Total Score GUI.Box(new Rect(Screen.width/4, Screen.height/4+10, Screen.width/2, Screen.height/2), "GAME OVER: "); //Restart Game if (GUI.Button(new Rect(Screen.width/4+10, Screen.height/4+Screen.height/10+50, Screen.width/2-20, Screen.height/10), "RESTART")){ Application.LoadLevel(Application.loadedLevel); } } } }
มีการชนโดน วัตถุต่างๆ แล้วเกิดเป็น ตัวแปล healthBar ลดไป แต่ถ้าชนสิ่งที่ชื่อว่า “Wincondition” หรือมรกต สีเขียวก็จะผ่านด่านเลย (ซึ่งผ่านด่านก็คือ เล่นใหม่ไม่ได้ทำด่านต่อ)
ส่วนของการบังคับตัวละครอธิบายมาหลายบทในเว็บไซต์แล้วขอ ละไว้ไปหาอ่านกันเองไม่ยากครับ มาส่วนของ Effect กันดีกว่า
public ParticleEmitter waterDeath; public ParticleEmitter deathLight; public ParticleEmitter AttackEffect;
เมื่อตกน้ำตาย จะมี Effect ตัวแปร waterDeath ให้ดึง Prefabs ที่ชื่อ Water มาวางเพื่อเกิดเป็นน้ำกระจาย เวลาตกลงไป
เช่นกันเมื่อ เลือดหมด Effect ที่ชื่อ deathLight แสงแห่งสวรรค์ก็จะปรากฏเมื่อตายเช่นกันครับ ตามด้วย AttackEffect เป็น Effect เมื่อโดนโจมตีโดยศัตรูนั่นคือ Monster และ Boss ครับ
ทีนี้ตัวละครของเรา ใน Hierachy จะมี Cube ที่ปรับ Mesh Renderer ให้ล่องหนอยู่ตัวหนึ่งทำหน้าที่เป็น ปืนครับ
มันคือ Cube ธรรมดาๆ แสน ธรรมดาที่ปรับ Inspector เป็นดังนี้
นึกไม่ออกให้ Double Click ที่ gun ใน Hierarchy ดูครับ
ใช้ Script ที่ชื่อ gun.cs ไปควบคุมมันอีกทีครับ
using UnityEngine; using System.Collections; public class Gun : MonoBehaviour { public GameObject Bullet; public float fireRate = 0.3f; private float nextFire = 0.0f; // Use this for initialization void Fire () { Instantiate(Bullet, transform.position, transform.rotation); } // Update is called once per frame void Update () { if (Input.GetButtonDown("Fire1") && Time.time > nextFire) { nextFire = Time.time + fireRate; Fire(); }
โดยเราต้องมีกระสุนผมได้สร้าง Prefab เป็นกระสุนไว้ให้แล้ว
(สามารถลาก Prefabs ไปวางที่ Inspecter ของ Gun ได้ครับถ้าทำกระสุนใหม่)
ส่วนของ Bullet มี Code ตามนี้ครับ เน้น RigidBody เมื่อยิงมันจะย้อยลงดินหายไป ส่วน gun.cs จะมีการ Cool down ไม่ให้ยิงรัวๆ ประมาณ 3 วินาที
using UnityEngine; using System.Collections; public class Bullet : MonoBehaviour { float Speed = 0.7f; float SecondsUntilDestroy = 1.2f; float startTime; public ParticleEmitter ClearBullets; // Use this for initialization void Start () { startTime = Time.time; } // Update is called once per frame void Update () { } void FixedUpdate(){ this.gameObject.transform.position += Speed * this.gameObject.transform.forward; if (Time.time - startTime >= SecondsUntilDestroy) { Instantiate(ClearBullets, transform.position, transform.rotation); Destroy(this.gameObject); } } void OnTriggerEnter(Collider collision){ if(collision.gameObject.name == "Monsters(Clone)"){ Destroy(gameObject); } } }
เมื่อมันโดน GameObject ที่ชื่อ Monsters(Clone) มันจะทำลายตัวเอง และถ้าไม่โดนอะไร จะมีการเคลียร์วัตถุไม่ให้เปลืองแรม SecondUntilDestroy ใน ระยะเวลา 1 วินาทีกว่าๆ
ทีนี้ส่วนของ Monster ครับ ตัว Prefabs ทำไว้แล้วมี RigidBody, Character Controller, Capsule ครบหมดแล้วใน Daydev Action Kit ตัวของ Boss ก็เช่นกัน เพียงแค่การทำงานจะต่างกันเล็กน้อย
ตัว Monster ใช้ Code ชื่อ AIEnemy.cs มีหน้าที่คือถ้าเจอ Player วิ่งเข้ามาใกล้ๆ ระยะโจมตีมันจะวิ่งเข้ามาโจมตีทันทีครับ โดยอ้างอิงกับ Animator Controller ที่สร้างไว้ให้แล้ว
using UnityEngine; using System.Collections; public class AIEnemy : MonoBehaviour { public float Distance = 900f; float MovementSpeed = 3.0f; int Health = 6; Animator anim; public bool dead = false; public ParticleEmitter AttackEffect; public ParticleEmitter Explosion; void Start () { anim = GetComponent <Animator> (); } void Update () { if (Health <= 0) { dead = true; } else { dead = false; } if (dead == true) { death(); } } void FixedUpdate () { // Get the Player object GameObject player = GameObject.Find("Player"); CharacterController characterController = GetComponent<CharacterController>(); Vector3 AIEyes = transform.position; AIEyes.y += characterController.height; var seeDirection = player.transform.position - AIEyes; seeDirection = seeDirection.normalized; int layerMask = 1 << LayerMask.NameToLayer("Player") | 1 << LayerMask.NameToLayer("Default"); Vector3 AIMovement = Vector3.zero; RaycastHit hitInfo; if (Physics.Raycast(AIEyes, seeDirection,out hitInfo, Distance, layerMask)) { if (hitInfo.collider.gameObject == player) { AIMovement = seeDirection; AIMovement.y = 0; AIMovement = AIMovement.normalized; anim.SetBool ("AttackPlayer", true); }else{ anim.SetBool ("AttackPlayer", false); } } if (AIMovement != Vector3.zero) { transform.rotation = Quaternion.LookRotation(AIMovement, Vector3.up); } characterController.SimpleMove(AIMovement * MovementSpeed); } void OnTriggerEnter(Collider theCollision){ if(theCollision.gameObject.name == "Bullet(Clone)"){ Instantiate(AttackEffect, transform.position, transform.rotation); Health = Health-2; Debug.Log ("HP: "+Health); } } void death(){ anim.SetBool ("AttackPlayer", false); anim.SetBool ("FoundPlayer", false); anim.SetTrigger ("Death"); StartCoroutine (DelayDie()); } IEnumerator DelayDie() { while (true) { yield return new WaitForSeconds(2.0f); Instantiate(Explosion, transform.position, transform.rotation); Destroy(gameObject); } } }
เมื่อมันโดน Bullet(Clone) หรือลูกบอลจาก Player ยิงใส่มันจะลดพลังลง เมื่อตาย จะมี Animator Controller ส่งค่า Trigger ว่า Death แล้วจะค่อยระเบิดหายไปอีกทีประมาณ 2 วินาที
ส่วนตัวของ Boss นั้นจะมี AIBoss.cs คุม จะมีส่วนของการยิงกระสุนเหมือนตัวละคร และจะยิงด้วยความถี่ Cool Down ประมาณ 4 วินาที
using UnityEngine; using System.Collections; public class AIBoss : MonoBehaviour { public float Distance = 900f; float MovementSpeed = 3.0f; int Health = 100; Animator anim; public bool dead = false; public ParticleEmitter AttackEffect; public ParticleEmitter Explosion; public GameObject BossBullet; public float fireRate = 1.0f; private float nextFire = 0.0f; public bool isWinner = false; void Start () { anim = GetComponent <Animator> (); Time.timeScale = 1; } void Fire () { Instantiate(BossBullet, transform.position, transform.rotation); } void Update () { if (Health <= 0) { dead = true; } else { dead = false; } if (dead == true) { death(); } } public void Winning() { Debug.Log("Winn!!!!!!"); isWinner = true; } void FixedUpdate () { // Get the Player object GameObject player = GameObject.Find("Player"); CharacterController characterController = GetComponent<CharacterController>(); Vector3 AIEyes = transform.position; AIEyes.y += characterController.height; var seeDirection = player.transform.position - AIEyes; seeDirection = seeDirection.normalized; int layerMask = 1 << LayerMask.NameToLayer("Player") | 1 << LayerMask.NameToLayer("Default"); Vector3 AIMovement = Vector3.zero; RaycastHit hitInfo; if (Physics.Raycast(AIEyes, seeDirection,out hitInfo, Distance, layerMask)) { if (hitInfo.collider.gameObject == player) { AIMovement = seeDirection; AIMovement.y = 0; AIMovement = AIMovement.normalized; anim.SetBool ("AttackPlayer", true); if (Time.time > nextFire) { nextFire = Time.time + fireRate; Fire (); } }else{ anim.SetBool ("AttackPlayer", false); } } if (AIMovement != Vector3.zero) { transform.rotation = Quaternion.LookRotation(AIMovement, Vector3.up); } characterController.SimpleMove(AIMovement * MovementSpeed); } void OnTriggerEnter(Collider theCollision){ if(theCollision.gameObject.name == "Bullet(Clone)"){ Instantiate(AttackEffect, transform.position, transform.rotation); Health = Health-2; Debug.Log ("HP: "+Health); } } void death(){ anim.SetBool ("AttackPlayer", false); anim.SetBool ("FoundPlayer", false); anim.SetTrigger ("Death"); StartCoroutine (DelayDie()); } IEnumerator DelayDie() { while (true) { yield return new WaitForSeconds(2.0f); Instantiate(Explosion, transform.position, transform.rotation); Destroy(gameObject); StartCoroutine (WaitForWin()); } } IEnumerator WaitForWin() { while (true) { yield return new WaitForSeconds(2.0f); Winning(); isWinner = true; } } void OnGUI(){ if (isWinner == true) { Time.timeScale = 0; //Show Total Score GUI.Box(new Rect(Screen.width/4, Screen.height/4+10, Screen.width/2, Screen.height/2), "Win!!!!: "); //Restart Game if (GUI.Button(new Rect(Screen.width/4+10, Screen.height/4+Screen.height/10+50, Screen.width/2-20, Screen.height/10), "Next Stage")){ Application.LoadLevel(Application.loadedLevel); } } } }
ตัว Monster จะมีการ Random ออกมา ทุกๆ 7 วินาทีเพื่อโจมตีเราดังนั้น เราต้องทำ RandomEnemy ครับ อัดไว้ใน Empty GameObject แล้วทำการโคลนตัว Prefabs ชื่อเดียวกัน
using UnityEngine; using System.Collections; public class RandomEnemy : MonoBehaviour { public GameObject RandomObjects; float Delay_start = 0.0f; float Delay_cooldown = 7.0f; void Start () { InvokeRepeating("Spawn", Delay_start, Delay_cooldown); } void Spawn() { Instantiate(RandomObjects, transform.position, transform.rotation); } }
ถ้าให้อธิบาย ชุด Kit ตัวนี้ก็คงมีแค่นี้ครับ เพราะเป็นแค่เครื่องมือแรกๆ ในการทำวิจัยของผม แต่ยังไงก็ยังแจกฟรีให้หลายๆ คนที่อยากจะศึกษานำไปศึกษากันเองต่อได้เองครับ
ตัวอย่าง Video แนะนำการเล่นครับ
ดาวน์โหลดที่นี่: http://bit.ly/DAYDEVKIT
หมายเหตุ: Code ที่ปรากฏ อาจจะมี Pattern ที่มาจาก Document ของ Unity บ้าง หรือในเว็บไซต์บ้าง หากมีคนที่บอกว่า ก็แค่ไปเอา Code มายำๆก็เสร็จ คือมันก็ถูกครับ แค่เชิง Pattern นะครับ แต่ Logic ของเกมผมคิดเองและพัฒนาเองครับ คงไม่ลอกใครมาแน่นอน หากใครจะดราม่าก็รบกวนดราม่าอย่างฉลาดด้วยนะครับ
สำหรับเวอร์ชัน 2: เป็นโครงการในอนาคตครับ หาทุนก่อน ใครอยากบริจาคกี่บาทก็ได้ก็กระซิบมาที่ http://www.facebook.com/daydevthailand เป็นการระดมทุนครับไม่บังคับ จะใช้ฟรีก็ไม่ว่ากันแค่ เครดิตผู้พัฒนาด้วยนะครับ ขอบคุณครับ