บทเรียนการสร้างเกม 2D ด้วย Godot Engine ส่วนของการทำให้ Camera2D ติดตามตัวละคร และ การทำ Instance Singleton สำหรับยิงกระสุนเรียก xml ของ Bullet
หมายเหตุ: กรุณาศึกษาบทเรียนก่อนหน้านี้:
- เขียนเกม 2D ด้วย Godot Engine
- เขียนเกม 2D ด้วย Godot Engine การใช้ Animated Sprite Node
- เขียนเกม 2D ด้วย Godot Engine การควบคุมคัวละคร Full Movement
บทเรียนต่อจากนี้จะเป็นการต่อยอด จาก บทเรียนก่อนหน้า ดังนั้นให้ทำการเปิด player.gd ขึ้นมาเพื่อ cleanup code เล็กน้อย คือการประกาศตัวแปรเพิ่ม
var anim var isShootPressed = false var fire_rate = 0.0 var next_fire = 0.5
แล้วประกาศใน func _ready():
anim = get_node( "AnimatedSprite" )
และแก้ไขบรรทัด ทั้งหมดที่เป็น
get_node( "AnimatedSprite" ).play("ทุกอย่าง")
เป็น
anim.play("ทุกอย่าง")
เมื่อพร้อมแล้ว เราก็จะเริ่มสร้าง Camera2D มาทำการวิ่งตามตัวละครก่อน ให้เรา Add Child Node ของ Player เลือก Camera2D เข้าไป
ทำการคลิกที่ Current ใน Inspector ของ Camera พร้อมกำหนด Offset ของกล้องเล็กน้อยเป็นขนาดที่เราต้องการ
ตอนนี้หากทดสอบจะเห็นว่ากล้องวิ่งตามตัวละครเรียบร้อยแล้ว หมดเรื่องของ Camara2D แล้วต่อไปคือเรื่องของกระสุน ซึ่งอย่างที่บอก Godot จะทำงานเป็น Node และ instance Node เราจะต้องเซฟ Scene นี้ไว้แล้วทำการ New Scene ขึ้นมาใหม่ วาง KinematicBody2D ลงไป ตามด้วย Sprite และ CollisionShape2D
ลาก Sprite ของกระสุนไปใช้ใน Sprite
กำหนดค่า CollisionShape2D เลือก Shape เป็น Circle
ปรับ Trigger ของ CollisionShape2D ใน Inspector เป็น on เพื่อใช้สำหรับการชนกับศัตรู หรือบางอย่าง (เหมือน Unity นั่นแหละ)
หลังจากนั้นเซฟ Save as new Scene ว่า bullet เสียแต่ ให้ใจเย็นๆ แล้วเลือก Scene Type เป็น .xml นะครับ ให้สังเกตว่า เราจะต้อง ได้ bullet.xml นะครับ ไม่ใช่ bullet.tscn
หลังจากนั้นให้เราประกาศคำสั่งต่อจากนี้ลงใน player.gd ของเรา ประกาศไว้ใต้ extend
extends KinematicBody2D var bullet = preload("res://bullet.xml")
เพื่อที่เราจะโหลด XML ที่เป็น node 2D แบบ KinematicBody2D ของ bullet ที่เราสร้างไว้มาใช้ได้เลย เก็บลงตัวแปร bullet
สร้าง Scene ใหม่ขึ้นมาชื่อว่า global.gd ให้เราใส่แค่ Node2D เปล่าๆ ไว้ 1 ตัวแล้วอัก Script ดังนี้ลงไป
extends Node2D var global_direction = 0
เจ้า node2D ของ global ที่มีตัวแปร global_direction นี่จะเป็น node กลางที่ตัวแปร global_direction นี้จะใช้ได้กับทุก node ทั้ง player.gd และ ไฟล์ gd อื่นๆ เพียงแค่เราต้องใช้ singletons, รูปแบบการออกแบบภาษาโปรแกรมที่จำกัดจำนวนของ Object ที่ถูกสร้างขึ้นในระบบ ซึ่งจะเป็นประโยชน์เมื่อระบบต้องการจะมี Object นั้นเพียงตัวเดียวเพื่อป้องกันไม่ให้เกิดการทำงานซ้ำซ้อนกันเช่น class สำหรับการเก็บข้อมูล หรือเป็น Model ที่มีการเรียกใช้งานทั้งระบบนั่นเอง
ไปที่ เมนู Scene เลือก Project Setting
เลือก global.gd เป็น Singletons
หลังจากนี้เราจะใช้ ตัวแปร global_direction จาก global.gd ได้ทุก node อย่างทั่วถึงแล้ว ซึ่ง global_direction นี้จะทำการเก็บ input_direction ของ player ว่าหันไปทางไหน ซ้าย หรือ ขวา เพื่อที่กระสุนจะได้พุ่งออกไปยังทิศนั้นแค่นั้นแหละครับ (ปล. Code ผมมันลูกทุ่งหน่อย)
ดังนั้นปรับฟังก์ชันเพิ่มของ player.gd สักหน่อย ใน func _process(delta):
#Input if input_direction: direction = input_direction global.global_direction = input_direction
และเพิ่มคำสั่งการยิง
#Fire if Input.is_key_pressed(KEY_Z): anim.play("attack") if isShootPressed == false: fire() isShootPressed = true #Check FireRate fire_rate += delta if fire_rate >= next_fire: fire_rate = 0.0 isShootPressed = false anim.play("Idle")
ไม่มีอะไรมากแค่กด Z ก็จะเรียกฟังก์ชัน fire() ที่เราสร้างขึ้นมาเอง โดยกำหนดอัตราการยิง หาก fire_rate น้อยกว่า next_fire ที่เรากำหนดไว้คือ 0.5 ยิงได้ ถ้าไม่ก็ยิงไม่ออก กันการกดยิงรัวๆ แค่นั้นแหละ anim.play(“attack”) รู้ใช่ไหมว่าเราต้องเพิ่ม Animation อะไรเข้าไป ก็ attack นั่นเอง (ตัด Photoshop เหนื่อยมาก)
ทีนี้เรามาเขียน function ใหม่กันคือ fire()
func fire(): var bullet_instance = bullet.instance() bullet_instance.set_name("bullet(Clone)") add_child(bullet_instance)
ง่ายๆ เลยให้สร้าง instance คือเจ้า bullet (ซึ่งก็คือ Scene bullet.xml) แล้วตั้งชื่อมันใหม่ว่า bullet(Clone) แล้วทำการ add_child() ติดกับผู้เล่นซะ
ภาพรวมของ player.gd จะเป็นดังนี้:
extends KinematicBody2D var bullet = preload("res://bullet.xml") var input_direction = 0 var direction = 0 var speed = 0 var velocity = 1 const MAX_SPEED = 600 const ACCELERATION = 1000 const DECELERATION = 2000 const JUMP_FORCE = 20.0 var anim var isShootPressed = false var fire_rate = 0.0 var next_fire = 0.5 func _ready(): set_process(true) anim = get_node( "AnimatedSprite" ) pass func _process(delta): #SeT Gravity move( Vector2(0,10)) #Input if input_direction: direction = input_direction global.global_direction = input_direction if Input.is_action_pressed("ui_left"): input_direction = -1 anim.set_flip_h( true ) anim.play("walk") elif Input.is_action_pressed("ui_right"): input_direction = 1 anim.set_flip_h( false ) anim.play("walk") else: input_direction = 0 anim.play("Idle") #Check Fire Animation # Movement if input_direction == - direction: speed /= 3 if input_direction: speed += ACCELERATION * delta else: speed -= DECELERATION * delta speed = clamp(speed, 0, MAX_SPEED) velocity = speed * delta * direction move(Vector2(velocity, 0)) #Splash Attack if Input.is_key_pressed(KEY_X): anim.play("attack") move(Vector2(20 * input_direction, 0)) #Fire if Input.is_key_pressed(KEY_Z): anim.play("attack") if isShootPressed == false: fire() isShootPressed = true #Check FireRate fire_rate += delta if fire_rate >= next_fire: fire_rate = 0.0 isShootPressed = false anim.play("Idle") #Jump if Input.is_action_pressed("ui_up"): anim.play("jump") move(Vector2(0, -JUMP_FORCE)) pass func fire(): var bullet_instance = bullet.instance() bullet_instance.set_name("bullet(Clone)") add_child(bullet_instance)
ทีนี้ไปที่ Scene ของ Bullet.xml ให้เรา Attach Script เข้าไปที่ KinematicBody2D ของ Bullet ตั้งชื่อว่า bullet.gd เขียน code ดังนี้:
extends KinematicBody2D var bulletSprite var wait_time = 0.0 var end_time = 0.3 func _ready(): set_process(true) bulletSprite = get_node( "Sprite" ) pass func _process(delta): var bulletDirection = global.global_direction if bulletDirection == -1: bulletSprite.set_flip_h( true ) translate(Vector2(-20,0)) else: bulletSprite.set_flip_h( false ) translate(Vector2(20,0)) wait_time += delta if wait_time > end_time: wait_time = 0.0 destroy() func destroy(): queue_free()
สังเกตสิ:
var bulletDirection = global.global_direction
คือไปเรียก global.gd เอาตัวแปร global_direction มาใช้นั่นเอง
ส่วนการทำงานง่ายๆ กระสุนมีอายุแค่ 0.3 วินาที ถ้าเกินนั้นให้ทำลายตัวเอง เรียกฟังก์ชัน destroy() ส่วนคำสั่ง delete_node นั้นใช้ queue_free() เป็นคำสั่งมาตราฐานเลย
เอ้าทดสอบ
คิดว่าคงได้ประโยชน์กันขอ จบส่วนของ Character ไว้เท่านี้ก่อน จะทำ Repo ไว้ให้ เพราะหลังจากบทนี้จะเป็นการทำ AI Navmesh ของ godot Engine ซึ่งขอเวลาไปศึกษาก่อนนะครับ
Download Tutorial Source: https://github.com/banyapondpu/godot_character_daydev
One Comment