Tuesday, May 13, 2014

Bài 10: Làm game đầu tiên - Sự kiện chạm màn hình và bắn đạn (Part 2)

Hi!

Đi làm ngồi rảnh quá, tranh thủ post bài vậy. Trong bài trước chúng ta đang triển khai dang dở 1 Project game đầu tiên nhỉ? Hoành tráng chưa. Cũng bình thường mà, Project nào chẳng bắt đầu từ sự đơn giản, sau đó phức tạp hóa lên dần dần.

Ở phần đầu chúng ta đã học được cách làm thế nào để tạo 1 nhân vật, 1 đám quái từ những hình ảnh thông qua lớp Sprite. Và trong phần thêm quái, bạn chú ý 1 đoạn tính toán vị trí xuất hiện, và tốc độ di chuyển cũng khá hay nhỉ. Chỉ là tính toán thôi mà, cũng chưa phải thuật toán gì ghê ghớm cả, nhưng thực sự nó là thuật toán ( đơn giản ) mà.

Trong phần tiếp theo này, chúng ta sẽ làm công việc sau:

+ Bắt sự kiện khi chạm màn hình
+ Khi chạm vào màn hình, nhân vật của ta sẽ bắn ra đạn
+ Xử lý viên đạn bay theo hướng của điểm chạm trên màn hình

Lets GOOOOÔ!

B1 - Sự kiện chạm màn hình và cách phát hiện

Các bạn mở file HelloWorldScene.cpp lên , trong hàm init() tìm đến cuối hàm trước lệnh return true; thêm vào đoạn lệnh sau:

//Tạo đối tượng truyền tải thông tin của các sự kiện
auto dispatcher = Director::getInstance()->getEventDispatcher();
//Tạo 1 đối tượng lắng nghe sự kiện Chạm màn hình theo cách One by One, xử lý 1 chạm tại 1 thời điểm
auto listener1 = EventListenerTouchOneByOne::create();
//Thiết lập "nuốt" sự kiện Touch khi xảy ra, ngăn ko cho các đối tượng Bắt sự kiện khác sử dụng event này
listener1->setSwallowTouches(true);

//Bắt sự kiện Touch, khi xảy ra sự kiện Touch nào thì sẽ gọi đến hàm tương ứng của lớp HelloWorld
listener1->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
listener1->onTouchMoved = CC_CALLBACK_2(HelloWorld::onTouchMoved, this);
listener1->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);

//Gửi cho dispatcher xử lý
dispatcher->addEventListenerWithSceneGraphPriority(listener1, this);

return true;

* Lưu ý: dù rằng ko dùng onTouchMoved, nhưng chúng ta cũng nen cho vào để sự kiện Touch được xử lý đầy đủ.

B2 - Xử lý sự kiện chạm - tạo khả năng bắn đạn cho nhân vật

Bạn mở file HelloWorldScene.h, thêm vào 3 nguyên mẫu hàm:

void onTouchEnded (cocos2d::Touch* touches, cocos2d::Event* event);
void onTouchMoved (cocos2d::Touch* touches, cocos2d::Event* event);
//Chú ý hàm onTouchBegan phải trả về bool
bool onTouchBegan (cocos2d::Touch* touches, cocos2d::Event* event);

Sau đó mở file HelloWorldScene.cpp để định nghĩa 3 hàm này

Mặc dù chúng ta ko dùng hết cả 3 hàm nhưng để sự kiện Touch được xử lý đầy đủ bạn phải khai báo cả 3 hàm như sau


bool HelloWorld::onTouchBegan(Touch* touch, Event* event)

{  

return true; // Phải trả về True

}



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

// Không xử lý gì ở đây

}

void HelloWorld::onTouchEnded (Touch* touches, Event* event){
   
// Lấy tọa độ của điểm chạm
Point location =  touches->getLocationInView();
location = Director::getInstance()->convertToGL(location);

Size winSize = Director::getInstance()->getWinSize();

//Tạo viên đạn là 1 Sprite, đặt vị trí đầu tiên gần nhân vật chính
auto projectile = Sprite::create("Projectile.png");
projectile->setPosition( Point(20, winSize.height/2) );

// Đoạn này tính toán điểm cuối cùng của viên đạn thông qua vị trí đầu và vị trí Touch, hình ảnh bên dưới sẽ minh họa cho điều này. Ở đây áp dụng 1 vài công thức toán học rất cơ bản thôi nhé. Không phức tạp lắm

// Lấy tọa độ điểm chạm trừ đi tọa độ đầu của viên đạn (offX, offY)
int offX = location.x - projectile->getPosition().x;
int offY = location.y - projectile->getPosition().y;

// Không cho phép bắn ngược và bắn thẳng đứng xuống dưới ( bên dưới nhân vật )

if (offX <= 0) return;

// Thỏa mãn điều trên thì tạo hình viên đạn trên màn
this->addChild(projectile,1);

//Tính toán tọa độ điểm cuối thông qua toa độ điểm đầu và khoảng offX, offY
// Tọa độ tuyệt đối realX = chiều rộng màn hình + 1/2 chiều rộng viên đạn, vừa lúc bay ra khỏi màn hình 
int realX = winSize.width  + (projectile->getContentSize().width/2); 

// Tỷ lệ giữa offY và offX
float ratio = (float)offY / (float)offX;

// Tọa độ tuyệt đối realY tính dựa trên realX và tỷ lệ trên + thêm tọa độ Y ban đầu của đạn ( tính theo Talet trong tam giác, hoặc theo tính tang 1 góc)

int realY = (realX * ratio) + projectile->getPosition().y; // Chỗ này theo mình là chưa đúng, đúng ra phải thế này int realY = ((realX-projectile->getPosition().x) * ratio) + projectile->getPosition().y; (realX-projectile->getPosition().x mới đúng là chiều dài từ điểm đầu tới điểm cuối trên trục X

//Tọa độ điểm cuối
auto realDest = Point(realX, realY);

//Chiều dài đường đi của viên đạn, tính theo Pitago a*a = b*b + c*c, a là cạnh huyền tam giác vuông
int offRealX = realX - projectile->getPosition().x;
int offRealY = realY - projectile->getPosition().y;
float length = sqrtf((offRealX * offRealX)  + (offRealY*offRealY));

// Thiết lập vận tốc 480pixels/1giây
float velocity = 480/1;

// Thời gian bay của đạn = quãng đường đạn bay chia vận tốc ở trên
float realMoveDuration = length/velocity;

// Di chuyển viên đạn tới điểm cuối với thời gian, và tọa độ đã tính ở trên. Khi qua viền màn hình thì biến mất
projectile->runAction( Sequence::create(
MoveTo::create(realMoveDuration, realDest),
CallFuncN::create(CC_CALLBACK_1(HelloWorld::spriteMoveFinished,this)), NULL) );

}

Đoạn code runAction nhìn có vẻ phức tạp nhưng nếu viết tường minh nó sẽ thế này

//Di chuyể đạn với thời gian và tọa độ tính toán ở trên
auto move= MoveTo::create(realMoveDuration,realDest);

//Khi move tới điểm cuối xong, bạn sẽ gọi hàm spriteMoveFinished để xóa bỏ hình ảnh viên đạn, nếu không xóa bỏ đi, thì viên đạn bay ra ngoài vẫn nằm trong Layer, ngày càng nhiều, xử lý ngày càng nặng. Đoạn này sẽ trả về 1 Action*
auto finish=CallFuncN::create(CC_CALLBACK1(HelloWorld::spriteMoveFinished,this));

//Thực hiện tuần tự 2 việc, Move sau đó Xóa 
auto run = Sequence::create(move,finish,NULL);

//Thực hiện công việc xử lý hàm Sequence
projectile->runAction(run);

Bạn nên xem lại bài về Các Action cơ bản của Sprite

Ảnh minh họa cho việc tính toán đường bay của đạn



Phần này như vậy là đã OK rồi nhé, tìm hiểu kỹ thì cũng không có gì phức tạp cả phải không nào. Tóm tắt lại công việc như sau

+ Bắt sự kiện chạm màn hình bằng các Listener
+ Xây dựng hàm xử lý khi có sự kiện Chạm màn hình, bắn ra viên đạn, tính toán đường bay, tọa độ, tốc độ viên đạn, có dính tí toán học, vật lý học nè
+ Tạo Action di chuyển viên đạn với tốc độ, tọa độ đã tính toán

Các bạn có thể down file nguồn ở đây, nếu ngại copy và kiểm tra code ở phía trên

Ảnh Demo chút


Tuy nhiên bạn sẽ thấy đạn bắn "xuyên táo" con quái luôn, hehe. Ở bài sau chúng ta sẽ thiết lập sự kiện va chạm cho viên đạn với quái nhé. 


12 comments:

  1. Cảm ơn bạn ! Đợi tiếp part 3 :)

    ReplyDelete
    Replies
    1. bài viết tuyệt vời...may mà có các bạn cùng song hành k thì đam mê viết game chắc gian nan lắm!

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

    ReplyDelete
  3. Lớp EventListenerTouchOneByOne trong v2.2.3 là ntn bạn, vì dự án mình đang học sử dụng bản 2.2.3, ko dùng v3+

    ReplyDelete
  4. bạn ơi cho mình hỏi, khi mình để đối tượng bắt sự kiện (listener1) onTouchMoved thì bị lỗi

    listener1 ->onTouchMoved = CC_CALLBACK_2(HelloWorld::onTouchesMoved, this);

    ERROR:

    Error 1 error C2664: '_Rx std::_Pmf_wrap<_Pmf_t,_Rx,_Farg0,_V0_t,_V1_t,_V2_t,_V3_t,_V4_t,_V5_t,_V6_t,_V7_t,_V8_t,_V9_t,_V10_t,>::operator ()(const _Wrapper &,_V0_t,_V1_t) const' : cannot convert parameter 2 from 'cocos2d::Touch *' to 'const std::vector<_Ty>


    Mình có tra google nhưng ko thấy kết quả, mong bạn giải thích giúp mình, nếu mình bỏ dòng đó đi thì chạy OK

    ReplyDelete
    Replies
    1. lỗi này có thể là do bạn khai báo hàm onTouchMoved không tương thích với hàm gọi trên đối tượng bắt sự kiện listener1, có thể trong lúc khai báo bạn dùng hàm HelloWorld::onTouchMoved, nhưng khi bạn sử dụng để đối tượng bắt sự kiện listener1 bạn lại dùng HelloWorld::onTouchesMoved

      listener1 ->onTouchMoved = CC_CALLBACK_2(HelloWorld::onTouchesMoved, this);

      chúc bạn thành công :)

      zidane

      Delete
  5. Mình gặp 1 lỗi mà chưa giải quyết được:
    Khi mình tạo 1 game mới, Scene chính là HelloWorld, mình tạo thêm 2 Scene khác là Level1 và Level2, mình dùng:
    auto scene = Scene::createWithPhysics();
    cho 2 Level đó, khi setPhySicsBody cho Player xong, thì việc setPosition cho Player là vô tác dụng, khi đó Player luôn tự nằm chính giữa màn hình.
    Mình đã thử thay đổi và search google mấy ngày nay mà chưa giải quyết được.
    Hy vọng là bạn hay ai đó đã từng gặp lỗi này mà solved rồi.
    Cảm ơn bạn nhiều.

    ReplyDelete
  6. OK, đã tìm ra, ai gặp lỗi thì coi luôn (Cocos2dx 3.6 nhé): Bị lỗi TransitionFlipX:
    director->replaceScene(TransitionFlipX::create(1, level1Scene));
    Dùng các Transition khác là ok.
    Vãi!!!

    ReplyDelete
    Replies
    1. tải phần mềm nào về mới có cái file HelloWorldScene.cpp vậy anh??
      cho em xin link được ko ạ

      Delete
  7. làm ơn cho em xin link để lập trình với

    ReplyDelete
  8. sắp qua 2018 rồi không biết bác Ad còn ở đây không ta :D

    ReplyDelete