Wednesday, May 28, 2014

Bài 19: Sprite Sheet Animation trong Cocos2dx-3

Hi!

Các bài trước chúng ta đã học và làm quen với các Sprite và Action cơ bản. Nhưng các bạn thấy rằng hầu hết các sprite đó trông có vẻ rất đơn điệu, chúng không hề có sự "cử động" - (animation) nào mặc dù chúng vẫn có "hành động" (action). Các bạn cần phân biệt Animation và Action nhé. Animation có thể hiểu ngắn gọn là những cử động của cơ thể nhân vật. Còn Action là những hành động của nhận vật để làm công việc gì đó. Đôi khi 2 khái niệm này cũng khá nhập nhằng. Hix.

Animation trong Cocos2d-x-3 có 2 loại
+ Sprite Sheet Animation: Tạo cử động bằng 1 loạt Sprite ảnh nối tiếp nhau
+ Skeleton Animation: Tạo cử động dạng khung xương

Bài này mình sẽ giới thiệu với mọi người loại Animation thứ nhất: Sprite Sheet Animation. Nội dung bài gồm:

+ Pack ảnh (texture paker) là gì? vì sao lại cần dùng pack ảnh
+ Cách tạo ra 1 Pack ảnh .Plist, prv.ccz
+ Cách import pack ảnh vào game và tạo Animation

Mình bắt đầu luôn đây!

B1- Pack ảnh là gì? dùng để làm gì

Khái niệm: 1 Pack ảnh bao gồm nhiều ảnh đơn gép lại với nhau, và có đi kèm 1 file để lưu thông số của từng ảnh. Thậm chí chỉ gồm có 1 file ( ví dụ pvr )
Dùng để làm gì? Quản lý file ảnh sẽ đơn giản hơn cho 1 project. Thường để nhóm các ảnh của một Animation, hoặc nhóm nhiều ảnh của chương trình lại, tối ưu chương trình, Ngăn chặn việc ăn cắp hình ảnh, và nhiều tác dụng khác.
- SpriteSheet có lẽ là 1 trường hợp riêng của Pack ảnh, gồm những hình được sắp xếp liên tiếp, có thứ tự của 1 chuyển động nào đó.
- Và đôi khi cũng nhập nhăng 2 cái này luôn, gọi chung hết là Sprite Sheet ( cứ hiểu là 1 nhóm ảnh vậy ). Haizzz

* Lưu ý, những định nghĩa hay tác dụng của pack ảnh do mình tự suy diễn nhé. Cũng ko biết phải tìm ở đâu. :-)). Ai biết rõ hơn thì chỉ với.

B2 - Cách tạo ra 1 pack ảnh .Plist, hoặc .Pvr

Bạn sử dụng chương trình TexturePacker ( hỗ trợ tốt cho Cocos2d) để tạo Pack. Do mình ko có license nên Publish ra sẽ báo lỗi. Chán ghê, mua full 2 bản TexturePacker và Physics Editor mất 1,1 triệu VNĐ. Haizzz, chưa kiếm được tiền từ game, mới học làm game mà đã chuẩn bị rút ví rồi.

Ngoài ra có 1 chương trình khác là ShoeBox cũng tạo được TexturePack nhé, các bạn search là thấy, Cài Adobe Air để chạy được phần mềm

Thôi chúng ta đi vào phần chính, giả sử đã tạo được pack ảnh từ TexturePacker nhé. Có khá nhiều định dạng Pack, nhưng bài mình tìm được là dạng .PNG + .PLIST

(Đã tìm được cách dùng FREE 4EVER phần mềm TexturePacker nhé, tuyệt vời ông mặt trời, mình sẽ hướng dẫn ở bài sau) . Đang test 1 thời gian xem lỗi gì ko?

B3 - Import pack ảnh vào game, tạo Animation

- Tạo 1 project mới tên animation nhé, nhớ thêm USING_NS_CC; vào phần #include của file HelloWorldScene.h
- Copy file Resource từ đây vào thư mục Resource
- Bắt đầu nghiên cứu code

* Mở file HelloWorldScene.h, Thêm vào đoạn code sau

public:

    HelloWorld(); // Hàm tạo
    ~HelloWorld(); // Hàm hủy
    virtual void onEnter(); // Hàm chồng ( override, not husband)
    // Bắt sự kiện Touch
    bool onTouchBegan(Touch* touch, Event* event);
    void onTouchMoved(Touch* touch, Event* event);
    void onTouchEnded(Touch* touch, Event* event);
    // Dừng lại
    void bearMoveEnded();

private:
    Sprite *bear; // Sẽ chứa ảnh con Gấu
    Action *walkAction; // Bước đi
    Action *moveAction; // Di chuyển
    bool moving;

* Mở file HelloWorldScene.cpp, Bạn định nghĩa 2 hàm tạo và hàm hủy như sau

HelloWorld::HelloWorld()
{
    moving =false;
}

HelloWorld::~HelloWorld()
{
    if (walkAction)
    {
        walkAction->release(); // Giải phóng con trỏ
        walkAction = NULL;
    }
}

Trong hàm init() xóa hết chỉ trừ return true và đoạn này

    if ( !Layer::init() )
    {
        return false;
    }

// Xóa hết

return true;

Thêm đoạn code sau vào phần đã xóa ở trên

// Bước 1, Nạp file .plist vào bộ đệm SpriteFrameCache, tạo 1 sheet = SpriteBatchNode, spritesheet để nạp 1 loạt các ảnh nằm trong 1 pack nhiều ảnh

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("AnimBear.plist");
auto spriteSheet = SpriteBatchNode::create("AnimBear.png");
this->addChild(spriteSheet);

// Bước 2, Nạp frame từng frame từ bộ đệm SpriteFrameCache vào 1 Vector ( giống mảng)

Vector<SpriteFrame*> aniframe(15); // Khai báo 1 vector kiểu SpriteFrame, với size = 15

char str[50]={0}; // chuỗi trung gian để đọc tên ảnh trong pack

for(int i =1;i<9;i++) // Lặp để đọc 8 ảnh trong pak
{
sprintf(str,"bear%d.png",i); // Đọc vào chuỗi str tên file thứ i

// Tạo 1 khung, lấy ra từ bộ đệm SpriteFrameCache với tên = str
auto frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(str);

aniframe.pushBack(frame); // Nhét vào vector

}

// Bước 3, Tạo Animation từ Vector SPriteFrame

// Tạo khung hình animation từ vector SpriteFrame
auto animation = Animation::createWithSpriteFrames(aniframe,0.1f);
// Tạo ảnh 1con gấu
bear = Sprite::createWithSpriteFrameName("bear1.png");
// Đặt vị trí giữa màn hình thôi
bear->setPosition(Point(visibleSize.width/2, visibleSize.height/2));

// Tạo Action Animate ( hoạt họa ) bằng cách gọi hàm create của lớp Animate, Hãy tưởng tượng thế này, bạn có 8 cái hình ảnh nằm trên 8 trang giấy, lật nhanh 8 trang => ảnh chuyển động của nhân vật. Cái hàm create của lớp Animate có tác dụng "lật trang"gần giống thế, sẽ duyệt qua các khung hình của animation tạo ra ở trên

walkAction = RepeatForever::create(Animate::create(animation));
walkAction->retain(); // Hàm này chưa hiểu ý lắm
spriteSheet->addChild(bear); // Thêm ảnh con gấu tạo ở trên vào spritesheet

+ Dựng hàm onEnter()

void HelloWorld::onEnter()
{
Layer::onEnter();  //  Phải gọi hàm onEnter của Layer, lớp cha của HelloWorld

// Đặt Listener khi vào game
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->setSwallowTouches(true);

touchListener->onTouchBegan= CC_CALLBACK_2(HelloWorld::onTouchBegan,this);
touchListener->onTouchMoved=CC_CALLBACK_2(HelloWorld::onTouchMoved,this);
touchListener->onTouchEnded=CC_CALLBACK_2(HelloWorld::onTouchEnded,this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener,this);


}
+ Xây dựng các hàm Touch

bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
{
    return true;
}

void HelloWorld::onTouchMoved(Touch* touch, Event* event)
{
}

void HelloWorld::onTouchEnded(Touch* touch, Event* event)
{

// Lấy điểm Touch
auto touchPoint = touch->getLocation();
touchPoint = this->convertToNodeSpace(touchPoint);

//Vận tốc= 480px / 3 giây;
float bearVelocity = 480.0/3.0;

// Khoảng di chuyển
Point moveDifference = touchPoint - bear->getPosition();
float distanceToMove = moveDifference.getLength(); // Khoảng cách thực
float moveDuration = distanceToMove / bearVelocity; // Thời gian di chuyển

// Quay đầu tùy theo khoảng cách âm hay dương
if (moveDifference.x < 0)
{
bear->setFlippedX(false); // Quay đầu
}
else
{
bear->setFlippedX(true); // Quay đầu

bear->stopAction(walkAction);
bear->runAction(walkAction); // Thực hiện cái Animate cử động nhân vật
// Di chuyển tới điểm Touch, trong khi vẫn thực hiện Animate
moveAction = Sequence::create(MoveTo::create(moveDuration,touchPoint),
CallFuncN::create(CC_CALLBACK_0(HelloWorld::bearMoveEnded, this)),NULL);
bear->runAction(moveAction);
moving = true;

}

+ Hàm

void HelloWorld::bearMoveEnded()
{
bear->stopAction(walkAction); // Dừng việc bước đi
moving = false;
}

Build ra và run




Vậy là chúng ta đã kết thúc bài 19 khá dài, Trong bài này chúng ta đã biết cách
+ Tạo khung hình với Vector SpriteFrame
+ Tạo animation từ SpriteSheet

Download file nguồn

Sprite Sheet tạo ra Animation khá hay và đơn giản nhưng nó có một nhược điểm khá lớn đó là sẽ tốn bộ nhớ để load các ảnh spritesheet.

Và chắc sẽ có bạn thắc mắc tạo khung physic body cho các nhân vật chuyển động như thế nào.? Các bài sau sẽ trả lời cho bạn nhé.

P/S: Trong bài này có 1 bug: là khi di chuyển con Gấu tới 1 điểm, nếu ta lick đúp sẽ thấy có lúc con gấu sẽ không bước chân mà chỉ trượt đi. Mọi người tìm cách fix lỗi giúp nhé.

Xin chào và hẹn gặp lại!

Bài 20: Học làm game thứ 3: Sushi Crush - Like Candy Crush or Bejewer ( Part 1)

25 comments:

  1. Cám ơn bạn,
    Fix được lỗi rồi :

    Thêm dòng này " if (moving) bear->stopAction(moveAction); " vào đầu hàm onTouchEnded

    ReplyDelete
    Replies
    1. Không được bạn à, hay bị Crash khi click đúp hoặc quay trái phải ( click chuột )

      Delete
    2. Mình cho câu lệnh bear->stopAllActions(); vào đầu hàm onTouchEnded
      , có vẻ như k bị lỗi nữa

      Delete
    3. Uh, sửa được rồi, chạy ngon, click đúp hơi giật giật nhưng ko Crash!

      Thanks!

      Delete
    4. This comment has been removed by the author.

      Delete
    5. Code của bạn khi nhấn qua bên trái con gấu thì nó không di chuyển.

      Nên thay đoạn:
      if (moveDifference.x < 0)
      {
      bear->setFlippedX(false); // Quay đầu
      }
      else
      {
      bear->setFlippedX(true); // Quay đầu

      bear->stopAction(walkAction);
      bear->runAction(walkAction); // Thực hiện cái Animate cử động nhân vật
      // Di chuyển tới điểm Touch, trong khi vẫn thực hiện Animate
      moveAction = Sequence::create(MoveTo::create(moveDuration,touchPoint),
      CallFuncN::create(CC_CALLBACK_0(HelloWorld::bearMoveEnded, this)),NULL);
      bear->runAction(moveAction);
      moving = true;

      }



      Bằng đoạn:

      if (moveDifference.x < 0)
      {
      bear->setFlippedX(false); // Quay đầu
      }
      else
      {
      bear->setFlippedX(true); // Quay đầu

      }

      bear->stopAction(walkAction);
      bear->runAction(walkAction); // Thực hiện cái Animate cử động nhân vật
      // Di chuyển tới điểm Touch, trong khi vẫn thực hiện Animate
      moveAction = Sequence::create(MoveTo::create(moveDuration,touchPoint),
      CallFuncN::create(CC_CALLBACK_0(HelloWorld::bearMoveEnded, this)),NULL);
      bear->runAction(moveAction);
      moving = true;


      Là được.

      Delete
    6. Thêm chút xíu là quay mặt gấu theo hướng đi cho vui vui chút. Code cuối cùng của hàm onTouchEnded sẽ là:

      void HelloWorld::onTouchEnded(Touch* touch, Event* event)
      {

      if(moving) bear->stopAllActions();
      // Lấy điểm Touch
      auto touchPoint = touch->getLocation();
      touchPoint = this->convertToNodeSpace(touchPoint);
      //Vận tốc= 480px / 3 giây;
      float bearVelocity = 480.0/3.0;
      // Khoảng di chuyển
      Point moveDifference = touchPoint - bear->getPosition();
      float distanceToMove = moveDifference.getLength(); // Khoảng cách thực
      float moveDuration = distanceToMove / bearVelocity; // Thời gian di chuyển

      // Quay đầu tùy theo khoảng cách âm hay dương
      if (moveDifference.x < 0)
      {
      bear->setFlippedX(false); // Quay đầu
      }
      else
      {
      bear->setFlippedX(true); // Quay đầu

      }

      // Xoay gấu quay mặt đúng theo hướng đi

      float tanAngle= (float)(touchPoint.y - bear->getPosition().y)/(float)(touchPoint.x - bear->getPosition().x);
      float angleRadians = atanf(tanAngle);
      float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
      float cocosAngle = -1 * angleDegrees;
      bear->setRotation(cocosAngle);


      bear->stopAction(walkAction);
      bear->runAction(walkAction); // Thực hiện cái Animate cử động nhân vật
      // Di chuyển tới điểm Touch, trong khi vẫn thực hiện Animate
      moveAction = Sequence::create(MoveTo::create(moveDuration,touchPoint),
      CallFuncN::create(CC_CALLBACK_0(HelloWorld::bearMoveEnded, this)),NULL);
      bear->runAction(moveAction);
      moving = true;



      }

      Delete
  2. hi, mình phải cảm ơn bạn mới đúng.
    Mong bạn tiếp tục ra các tut mới.

    ReplyDelete
  3. Bài viết khá hay thanks bạn

    ReplyDelete
  4. Mình có câu hỏi là tại sao phải thêm 1 sprite bear vào SpriteSheet với mục tiêu gì? bỉnh thường mình vẫn chạy Animation oke mà?
    spriteSheet->addChild(bear); // Thêm ảnh con gấu tạo ở trên vào spritesheet

    ReplyDelete
    Replies
    1. Cái này liên quan tới vấn đề OpenGL,

      Nếu chỉ dùng sprite và ko dùng SpriteSheet ( SpriteBatchNode ) thì khi chạy animation, sẽ có nhiều đối oject OpenGL được tạo ra để dựng ảnh nằm trong cái animation kia.

      Dùng SpriteBatchNode sẽ giải quyết được vấn đề này, chỉ dùng 1 object OpenGL để dựng hình cho sprite từ animation

      Hãy tham khảo bài này để hiểu rõ hơn

      http://dev.bunnyhero.org/2013/10/bunny-meets-cocos2d-x-part-1-sprite-sheets-and-animations/

      Delete
    2. *> Việc add thêm 1 sprite bear vào SpriteSheet giống như là tạo một đối tượng để mình handle việc xoay animation bằng hàm Sprite->setFlippedX(bool), vì chỉ có lớp Sprite mới có phương thức này theo biết là như thế.
      *> " if you do not use a SpriteBatchNode, each Sprite will be drawn with a separate OpenGL drawing operation, which is much slower." kiến thức này mới. Thanks bạn đã chia sẻ :D

      Delete
  5. [fix bug] bear trượt khi nhấp đúp
    //bear->stopAction(walkAction);
    bear->cleanup();

    ReplyDelete
  6. 1. Setup Cocostudio cho project cocos2d:x. Tuy bằng tiếng trung như cứ nhìn hình làm theo là được.
    http://blog.csdn.net/fansongy/article/details/16950241

    ReplyDelete
  7. VD em có 1 nhân vật có 3 hành động.vậy thì phải dùng tận 3 Sprite,3 SpriteBatch,3 Animate hả anh.
    Như thế việc setposition rất mệt và quản lí cũng khó nữa :(

    ReplyDelete
    Replies
    1. Không bạn nhé, hãy nhớ kỹ vấn đề này:

      + Bạn chỉ cần dùng 1 SpriteBatchNode để quản lý tất cả animation trong game
      + Khi bạn cần tạo animationAction cho 1 Sprite nào đó, bạn phải tạo ra Sprite đó, rồi add vào SpriteBatchNode. Sau do chi việc gọi animation như thế này: sprite->runAction(animationAction);. SpriteBatchNode chỉ dùng để add các đối tượng vào thôi.
      + Mỗi nhân vật có thể có nhiều Action ( walk, jump, die , rotate, move...). Animation chỉ là 1 trường hợp riêng của action, cách tạo thì bạn dùng các SpriteFrame như trên. Hãy xem

      sprite->runAction(RotateBy::create(2.0f, 660);); // bạn ko cần add sprite này vào BatchNode vẫn chạy được

      sprite->runAction(walkAction). Cái walkAction được tạo ra từ animation như bài trên. và bạn nên add sprite này vào BatchNode.

      => Animation là 1 Action có liên quan tới khung hình, được gọi qua hàm runAction của đối tương, và đối tượng phải bỏ vào SpriteBatchNode ( ko bỏ vào có chạy ko nhỉ)



      Delete
    2. E đã giải quyết đc ạ.tại thấy dùng file plist rắc rối quá.e load chay SpriteFrame sau :p.tks anh

      Delete
  8. e tạo một class Avatar để làm nhưng k hiểu vì sao build k hiện lỗi, nhưng lúc chạy nó văng ra báo break... :(
    link folder Classes, mong các bác nào chỉ giúp :(
    https://www.dropbox.com/s/xb29cw1aqb4s4sh/Classes.rar

    ReplyDelete
    Replies
    1. + Mình build thì thấy 1 lỗi này
      Sai tên Resource AnimBear chứ ko phải AnimeBear. Sai path Resource ko báo lỗi, nhưng khi chạy sẽ lỗi. Và bạn ko nên đổi tên file Resource kia khi chúng đựa tạo ra bởi phần mềm TexturePacker ( sẽ lỗi )

      + Sửa lại RS , build thì gặp null poiner, mình khoanh vùng thì nó ở trong hàm init của Avatar, kiểm tra ra thì code đúng ko có gì sai => chịu. Trong khi mình build lại code bài 19 này ở V3.2 Final thì OK

      => Mình thấy bạn thiết kế lớp Avatar ko được chuẩn cho lắm. Lớp này nên kế thừa từ Sprite, có Action, có các hàm và thuộc tính liên quan tới Sprite gấu, không nên có Touch, onEnter ( dành cho Scene).

      Thế nhé, hãy xây dựng đối tượng sát hơn

      Delete
  9. https://www.youtube.com/watch?v=crrFUYabm6E
    https://www.youtube.com/watch?v=_KyUqyS5MLA
    Chắc cũng không cần giải thích :) Xem 2 video bạn sẽ hiểu được tác dụng của sprite sheet. Hi vọng nó có ích cho bạn và mọi người.

    ReplyDelete
  10. walkAction->retain(); // Hàm này chưa hiểu ý lắm
    Hàm này nó giúp bạn báo cho trình biết con trỏ ptr nắm giữ tài nguyên nó đang trỏ đến. Reference counting sẽ tăng lên 1 đơn vị. Hàm này ngược lại với walk->autorealease nhé mọi người

    ReplyDelete
  11. Mình nhầm chút chỗ "Hàm này ngược lại với walk->autorealease nhé mọi người"
    Nó ngược với walkAction->release();

    ReplyDelete
  12. sao mình build nó không tạo được animation nhỉ tải cả source code về paste đè vào code của mình cung không có chạy

    ReplyDelete
    Replies
    1. của mình nó cũng không animation, có lý do nào nữa không nhỉ, mọi người giúp mình với.

      Delete
  13. cho hỏi char str[50]={0}. ={0} có nghĩa là sao vậy

    ReplyDelete