GPBasics 06: Thực hành điều khiển nhân vật

Chào các bạn, hôm nay chúng ta sẽ cùng thực hành về điều khiển nhân vật, kết quả của bài này các bạn có thể xem tại video:

Đầu tiên các bạn tạo một class mới như sau:

#ifndef NINJA_H
#define NINJA_H
 
#include <irrlicht.h>
 
// liệt kê các trạng thái của nhân vật
enum NINJA_STATE
{
    INVALID,
    IDLE,
    FORWARD,
    BACKWARD
};
 
class Ninja
{
public:
                                Ninja();
                                ~Ninja();
 
                                // hàm này cần nhận scene manager để load model
    void                        Init(irr::scene::ISceneManager* smgr);
 
                                // các hàm điều khiển nhân vật
    void                        Idle();
    void                        Forward(float dt);
    void                        Backward(float dt);
 
    void                        Destroy();
 
private:
                                // nhân vật giữ một con trỏ tới scene node
    irr::scene::IAnimatedMeshSceneNode* m_Node;
                                // trạng thái hiện tại
    NINJA_STATE                 m_State;
                                // tốc độ di chuyển
    float                       m_MovementSpeed;
};
 
#endif // NINJA_H

Hàm khởi tạo, hàm hủy và hàm Destroy:

Ninja::Ninja()
    :   m_Node(NULL),
        m_State(INVALID),
        m_MovementSpeed(10.0f)
{
 
}
 
void Ninja::Destroy()
{
    m_Node->drop();
    m_Node = NULL;
}
 
Ninja::~Ninja()
{
    Destroy();
}

 Không có gì phức tạp ở đây …

Về model ninja mà chúng ta sẽ dùng để điều khiển, đây là một 3D model mình lấy từ website http://www.psionic3d.co.uk/. Các bạn có thể sử dụng model này vào bất cứ mục đích gì, và khi sử dụng vui lòng ghi credits đầy đủ và rõ ràng.

Animation Ranges của model Ninja:

1-14 Walk (normal)

15-30 Stealth Walk

32-44 Punch and swipe sword

45-59 Swipe and spin sword

60-68 Overhead twohanded downswipe

69-72 Up to block position (play backwards to lower sword if you want)

73-83 Forward kick

84-93 Pick up from floor (or down to crouch at frame 87)

94-102 Jump

103-111 Jump without height (for programmer controlled jumps)

112-125 High jump to Sword Kill (Finish em off move??)

126-133 Side Kick

134-145 Spinning Sword attack (might wanna speed this up in game)

146-158 Backflip

159-165 Climb wall

166-173 Death 1 – Fall back onto ground

174-182 Death 2 – Fall forward onto ground

184-205 Idle 1 – Breathe heavily

206-250 Idle 2

251-300 Idle 3

Bạn hãy tham khảo kỹ những con số này bởi irrlicht sẽ dùng nó để render các hành động tương ứng đã có sẵn sau khi load model.

Ý tưởng về điều khiển nhân vật ở trong bài này như sau:

Mỗi vòng lặp game kiểm tra input, nếu nút W được bấm, nhân vật tiến lại gần Camera và lùi ra xa nếu S được bấm. Nếu không nút nào được bấm, nhân vật sẽ đứng yên và chạy animation Idle.

Hàm Init của Ninja:

void Ninja::Init(irr::scene::ISceneManager* smgr)
{
    irr::scene::IAnimatedMesh* mesh = smgr->getMesh("ninja.b3d");
 
    if (!mesh)
        return;
 
    m_Node = smgr->addAnimatedMeshSceneNode(mesh);
 
    if (m_Node)
    {
        m_Node->setMaterialFlag(irr::video::EMF_LIGHTING, false);
	 // vị trí mặc định của ninja, bạn hoàn toàn có thể thay đổi
        m_Node->setPosition(irr::core::vector3df(0, 0, 0));
        // lúc khởi tạo mặc định ninja sẽ idle
        Idle();
    }
}

Hàm Idle:

void Ninja::Idle()
{
    // phải kiểm tra trạng thái
    // bởi nếu hiện tại vẫn đang Idle mà câu lệnh setFrameLoop được gọi, 
    // animation sẽ luôn chạy lại từ đầu
    // hậu quả là bạn sẽ không thấy hành động nào diễn ra cả
    if (m_State != IDLE)
    {
        m_State = IDLE;
        m_Node->setFrameLoop(251, 300);
    }
}

Hàm Forward – tất nhiên là chúng ta sử dụng framerate independent movement, cần đến frame time:

void Ninja::Forward(float dt)
{
    // nếu trạng thái hiện tại không phải là đang tiến về trước
    // đặt lại trạng thái và play animations
    // nếu lệnh if này sai tức là hiện tại nhân vật vẫn đang forward
    // vậy thì chỉ cần thay đổi vị trí của nó
    // lệnh play animation đã được thực hiện từ trước rồi
    // hàm setFrameLoop sẽ chạy hết animation và tự động lặp lại
    if (m_State != FORWARD)
    {
        m_State = FORWARD;
        m_Node->setFrameLoop(1, 13);
    }
 
    irr::core::vector3df pos = m_Node->getPosition();
 
    // di chuyển theo trục Z
    pos.Z += m_MovementSpeed * dt;
 
    m_Node->setPosition(pos);
}

Hàm Backward tương tự như hàm Forward …

OK, vậy là API điều khiển nhân vật đã xong, tiếp đến chúng ta set up scene và test thử.

Bạn download một cái plane mình làm ở đây để tượng trưng cho mặt đất:

Tiếp đến là load Plane ở trong hàm main, hãy thử vận dụng các tọa độ để đặt ground và camera ở vị trí thích hợp để có thể quan sát.

Khởi tạo Ninja:

Ninja* ninja = new Ninja();
ninja->Init(smgr);

Code điều khiển nhân vật:

while (...)
{
    // tính thời gian đã trôi qua (delta time)
 
    if (receiver->IsKeyDown(irr::KEY_KEY_W))
        ninja->Forward(dt);
    else if (receiver->IsKeyDown(irr::KEY_KEY_S))
        ninja->Backward(dt);
    else
        ninja->Idle();
 
    driver->beginScene(true, true, irr::video::SColor(255, 100, 101, 140));
 
    smgr->drawAll();
 
    driver->endScene();
}
 
delete ninja;
 
device->drop();
return 0;

Extra:

Animation Blending

Như bạn có thể để ý trong ví dụ ở trên thì việc chuyển tiếp giữa các hành động rất đột ngột và gây mất tự nhiên khi nhân vật chuyển động, kỹ thuật Animation Blending sẽ giúp chúng ta giải quyết vấn đề này. Một số khái niệm cần thiết:

Skeletal Animations: Nhân vật có một khung xương và bề mặt ngoài của nó (mesh) sẽ chuyển động khi các xương ở bên trong chuyển động. Animation Blending chỉ hoạt động với loại animation này vì nguyên lý của nó là tính toán lại để tạo ra các animation chuyển tiếp từ frame này sang frame khác. Các file đồ họa hỗ trợ skeletal animations: .b3d, .X, .ms3d

Blending time/TransitionTime: thời gian chuyển tiếp, tức thay vì đột ngột thay đổi các chuỗi animation, mỗi lần thay đổi thì irrlicht sẽ chèn thêm một đoạn animation chuyển tiếp có độ dài bằng khoảng thời gian mà bạn chọn.

Cách sử dụng:

Bạn có thể dùng Animation Blending như sau – chỉ dành cho IAnimatedSceneNode được load từ các file có hỗ trợ Skeletal Animations.

Node->setTransitionTime(seconds);

Và trước câu lệnh drawAll của SceneManager, bạn gọi:

Node->animateJoints();

Áp dụng vào bài này, các bạn có thể sử dụng như sau:
Mình viết thêm hàm Animate cho class Ninja:

class Ninja
{
public:
                                Ninja();
                                ~Ninja();
 
                                // hàm này cần nhận scene manager để load model
    void                        Init(irr::scene::ISceneManager* smgr);
 
                                // các hàm điều khiển nhân vật
    void                        Idle();
    void                        Forward(float dt);
    void                        Backward(float dt);
 
    void                        Animate();
 
    void                        Destroy();
 
private:
                                // nhân vật giữ một con trỏ tới scene node
    irr::scene::IAnimatedMeshSceneNode* m_Node;
                                // trạng thái hiện tại
    NINJA_STATE                 m_State;
                                // tốc độ di chuyển
    float                       m_MovementSpeed;
};

Sau đó trong hàm Init, đặt blending time cho nó:

void Ninja::Init(irr::scene::ISceneManager* smgr)
{
    irr::scene::IAnimatedMesh* mesh = smgr->getMesh("ninja.b3d");
 
    if (!mesh)
        return;
 
    m_Node = smgr->addAnimatedMeshSceneNode(mesh);
 
    if (m_Node)
    {
        m_Node->setMaterialFlag(irr::video::EMF_LIGHTING, false);
        m_Node->setPosition(irr::core::vector3df(0, 0, 0));
        // lúc khởi tạo mặc định ninja sẽ idle
        Idle();
        m_Node->setTransitionTime(0.2f);
    }
}
 
void Ninja::Animate()
{
    m_Node->animateJoints();
}

Trong vòng lặp rendering:

while (...)
{
    // tính toán delta time
 
    if (receiver->IsKeyDown(irr::KEY_KEY_W))
        ninja->Forward(dt);
    else if (receiver->IsKeyDown(irr::KEY_KEY_S))
        ninja->Backward(dt);
    else
        ninja->Idle();
 
    // gọi hàm animate
    ninja->Animate();
 
    driver->beginScene(true, true, irr::video::SColor(255, 100, 101, 140));
 
    smgr->drawAll();
 
    driver->endScene();
}
 
delete ninja;
 
device->drop();
return 0;

Bạn có thể xem video sau để thấy sự khác biệt khi sử dụng Animation Blending
(Tay cầm kiếm của ninja nâng lên và hạ xuống khi bắt đầu bước đi chứ ko dịch chuyển tức thời vị trí của tay như video trước, đây chính là những đoạn animation được irrlicht tính toán để thêm vào mỗi khi chuyển đổi animation)

Comments

Related Posts

GPBasics 05: Căn bản về điều khiển nhân vật

Chào các bạn, trong tutorial này chúng ta sẽ làm việc với một số vấn đề trong điều khiển nhân vật. Chúng ta sẽ làm quen với khái niệm mới: Framerate independent movement. Trước khi đọc bài này thì chúng ta phải sử dụng các kiến thức về game loop (tài liệu về phần này […]

Read More

GPBasics 04: Irrlicht Text

1. Irrlicht Text Scene Node Irrlicht Text Scene Node là một Scene Node giúp bạn render các text trong 3D World. Để khởi tạo nó, bạn cần load một Font cho text. Thành phần này nằm trong GUI nên bạn cần phải thao tác qua Irrlicht GUI Environment. irr::gui::IGUIEnvironment* gui = device->getGUIEnvironment();   // font […]

Read More