Cocos2d-x 3.2 Final

Cập nhật lên phiên bản Cocos2d-x 3.2. Bổ sung 3D sprite, 3D animation, game controller, fast Tilemap,..v...v... Cập nhật thường xuyên nhé

Cocos2d-x 3.0 Final

Cocos2d-x là Engine lập trình game đa nền tảng phổ biến nhất trên thế giới. Tính năng mạnh mẽ, dễ sử dụng, miễn phí, mã nguồn mở.

Cocos2d-x V3.0 Final

Hướng dẫn chi tiết, đầy đủ bằng Video, Blog. Cộng đồng hỗ trợ rộng lớn. Sản phẩm phong phú mọi nền tảng

Game Badland

Một trong những game nổi tiếng nhất được tạo ra bởi Engine Cocos2d-x.

Cocos2d-x V3.1

Cập nhật phiên bản Engine Cocos2d-x V3.1 bổ sung thêm nhiều tính năng. Sửa các lỗi nhỏ của các phiên bản trước

Contra Evolution - KONAMI

1 Game rất hay không thể bỏ qua, 1 vé trở về với tuổi thơ.

Tuesday, June 24, 2014

Bài 24 Giới thiệu CocoStudio - Công cụ tạo Animation cực cool - Import game (Part 2)


Hi đầu tuần!
Ở bài 23, mình đã giới thiệu sơ qua với mọi người về CocoStudio - 1 Tool khá hay để tạo Animation. Các bạn đã biết cách dựng khung xương, gắn xương với nhau, gắn ảnh Texture với xương rồi phải không, tuy lúc đầu sẽ gặp chút khó khăn ( thậm chí là bực mình ) khi thao tác nhưng chỉ cần bỏ ra 1 tiếng là bạn sẽ quen ngay với công việc "xương xẩu" này.

Trong bài tiếp theo này, mình sẽ hướng dẫn mọi người cách để tạo Animation cho nhân vật. Nếu ai đã từng học Flash rồi thì bước này thật sự dễ dàng nhé, vì nó khá giống. Tuy giống là vậy nhưng mình thấy Flash có một cái hơn đó là có thể can thiệp sâu bằng mã AS mà trong CocoStudio không thể can thiệp bằng script được ( Ai học Flash + AS rồi thì cho mình so sánh nhé, vì mình ko học ). Nhưng, vâng lại nhưng, CocoStudio lại có thể IMPORT Flash Project vào trong Studio, và hơn nữa Import được cả file Plist ( Particle - hiệu ứng, Physics ?? ) có sẵn từ bên ngoài vào. Vậy là bạn không phải lo Studio không thể tạo được Animation và Effect đẹp mắt nha.

Trong bài này mình sẽ hướng dẫn các bạn các công việc sau:

+ Sử dụng Timeline tạo Animation với Animation Mode
+ Xuất ra dịnh Resource phù hợp với Coco2d-x V3
+ Import vào game, QUAN TRỌNG nha

Bắt đầu nào!!!

B1 - Sử dụng Timeline tạo Animation + Export Resource

Bạn mở project MyAnimation của bài trước ra, với khung xương, texture đã được tạo sẵn ( Các bạn hãy nghiên cứu lại bài 23 một chút để chuẩn bị tốt cho bài này - Nhất là các thao tác với chuột ). OK?

Bạn bật sang chế độ Animation, sẽ hiện ra bảng Timeline như hình dưới




Trong ô Animation List, là danh sách các cử động - chuyển động của nhân vật, bạn có thể xóa, tạo mới, đổi tên, copy bằng menu chuột phải.


Tại ô Timeline bên cạnh liệt kê ra các Layer tạo nên nhân vật. Ở đây bạn sẽ dùng Timeline + Keyframe + di chuyển các đoạn xương để tạo nên chuyển động của Nhân vật. Các điểm cần lưu ý liên quan tới Animation:


+ Mỗi Layer được tạo chuyển động bằng 1 đường Timeline, màu trắng ( bên phải ). 
+ Layer nào ko có timeline có nghĩa là không xuất hiện trong chuyển động
+ Các số 0,5,10,15 là số khung hình / giây. Có thể ngầm hiểu là Thời gian luôn cũng được
+ Các dấu tròn là các Keyframe. Keyframe là gì, chính là những Frame đánh dấu sự thay đổi của hướng  chuyển động, hoặc thay đổi cách chuyển động ( hiệu ứng - tốc độ )

Hướng chuyển động: 
- Bạn hãy hình dung khi di chuyển từ A tới B, rồi tới C ( A,B,C không thẳng hàng) thì ta sẽ đặt 3 Keyframe tại thời điểm chuyển động qua A, B, C. 
- Hoặc khi chuyển động từ A tới B, rồi quay lại A thì sao? Bạn cũng sẽ phải đặt 3 Keyframe tại thời điểm chuyển động qua A, B, A. Nghĩa là khi có sự đổi hướng, đảo chiều chuyển động, thay đổi vận tốc, hiệu ứng thì thường đặt Keyframe tại điểm thay đổi đó. 
- Vậy với cung tròn thì sao, bạn hãy cố gắng chia cung tròn ra làm các đoạn ngắn rồi thêm Keyframe vào các đoạn chuyển hướng đó. 

Cách chuyển động:
Tạm hiểu là hiệu ứng hoặc tốc độ chuyển động ( bạn nhìn thấy ở bảng nhỏ bên phải Timeline là Custom Curve ko - chính hắn ). Bạn đặt con trỏ vào Keyframe, rồi chọn 1 hiệu ứng, rồi chạy thử xem, sẽ thấy khác biệt.

Tất nhiên trên 1 đường chuyển động thẳng, bạn có thể đặt nhiều hơn 2 Keyframe cũng ko sao, khi đó hướng chuyển động là ko đổi, nhưng kiểu cách chuyển động sẽ thay đổi ( bằng cách áp dụng Custom Curve) hoặc vận tốc thay đổi ( thay đổi số khung hình ). Bạn sẽ thấy cùng 1 đường thằng nhưng cách chuyển động - vận tốc chuyển động có khác nhau.

* Một số lưu ý với Keyframe
+ Layer ko có Keyframe nào, sẽ không hiện lên trong chuyển động.
+ Keyframe hide ( màu đỏ ) để ẩn Layer tại timeline đặt Keyframe cho tới khi gặp 1 Keyframe hiện ( màu trắng ) . Bạn dùng menu chuột phải để Set thuộc tính.
+ Chuyển động giữa 2 Key gọi là Tween Frame
+ 1 Chuyển động được tạo nên bởi >=2 Keyframe.
+ Số khung hình giữa 2 Keyframe càng lớn, chuyển động càng mượt, và càng chậm ( với cùng 1 đoạn đường ) - Dựa vào đây bạn có thể chỉnh vận tốc nhanh chậm của chuyển động. Tăng số khung hình sẽ cho vận tốc chậm và ngược lại. 

Bạn Test thế này:
- Tại Key 1 (timeline 0) bạn đặt đối tượng ( 1 xương gắn 1 texture cho dễ nhìn ) tại vị trí A.
- Kéo đối tượng ra vị trí B khác A ( xa xa vào cho dễ nhìn ), đặt con trỏ tại timeline 40, chuột phải/Add frame - Hoặc bạn dùng nút Record Frame bên dưới nút Play ấy, đây sẽ là Key2
- Nhấn Play xem kết quả
- Bây giờ kéo Key2 lại gần Key1 ( về timeline 10 ), nhấn Play xem kết quả,
- Giờ lại kéo Key2 ra timeline 80, nhấn Play
- Giờ đặt chuột vào Key1, chọn Custom Curve tùy ý, rồi nhấn Play, bạn thấy chuyển động có tí hiệu ứng chưa.

OK, xong phần lý thuyết, Bây giờ chúng ta thực hành chút, Mình sẽ tạo chuyển động bước đi của nhân vật, đặt tên là walk
Bạn làm như sau:

1/ Chuột phải trong ô Animation List, chọn Add Animation, đặt tên là walk
2/ Key1: bạn đặt chuột vào cột Timeline0 của Layer trên cùng, chuột phải / Add Frame. Các Layer bên dưới cũng làm như thế. Vậy là tất cả Layer đều hiện trong chuyển động




3/ Bước này khó nè: Bạn phải xác định trong chuyển động walk thì bộ phận nào ( layer ) chuyển động nhiều nhất, phức tạp nhất thì bộ phận đó sẽ có nhiều Keyframe nhất. Ví dụ là 2 chân và 2 tay. Và bạn phải hình dung cách bước đi của nhân vật như thế nào cho chuẩn: Chân trái + tay phải cùng đưa lên, sau đó thu về, sau đó chân phải + tay trái đưa lên, rồi lại thu về. Chả thấy ai chân và tay cùng 1 bên đưa lên, đưa về cả trừ ông dị dạng. :-((

4/ Ví dụ cái cánh tay trái ( Layer 19 ) sẽ có 5 vị trí Keyframe như sau ( bạn nhớ chọn vào nút Xoay xương - Inverse Kinematics nhé)
+ Key 1 : duỗi thẳng - Timeline 0
+ Key 2: đưa lên trước - Timeline 10
+ Key 3: về vị trí Key1 - Timeline 20, bạn copy Key1 rồi paste vào timeline 20 để đảm bảo đúng vị trí cũ của tay
+ Key 4: đưa về sau - Timeline 30
+ Key 5: về vị trí giống Key1 - Timeline 40, bạn copy Key1 rồi paste vào timeline 40 để đảm bảo đúng vị trí cũ của tay
Thử nhấn Play xem

5/ Cánh tay cầm súng - phải ( Layer 20 ) Cũng tương tự với 5 vị trí Keyframe
+ Key1 : duỗi - TL0
+ Key2 : đưa về sau - TL10
+ Key3: duỗi: Copy Key1 - TL20
+ Key4: đưa lên trước - TL 30
+ Key5: duỗi: Copy Key1 - TL40
* Tay này gồm 2 xương gắn nhau, nên bạn phải di chuyển sau cho khéo 1 tý cho tay hơi cong ở phần khuỷu tay nhé

6/ Chân Phải ( Layer 16 ) Cũng tương tự với 5 vị trí Keyframe , di chuyển giống tay Trái
+ Key1 : đứng thẳng- TL0
+ Key2 : đưa lên Trước- TL10
+ Key3: duỗi: Copy Key1 - TL20
+ Key4: đưa về Sau - TL 30
+ Key5: duỗi: Copy Key1 - TL40
* Chân này gồm 2 xương gắn nhau, nên bạn phải di chuyển sau cho khéo 1 tý cho chân hơi cong ở phần đầu gối nhé

7/ Chân Trái ( Layer 26 ) Cũng tương tự với 5 vị trí Keyframe , di chuyển giống tay Trái
+ Key1 : đứng thẳng - TL0
+ Key2 : đưa về Sau- TL10
+ Key3: duỗi: Copy Key1 - TL20
+ Key4: đưa lên trước- TL 30
+ Key5: duỗi: Copy Key1 - TL40
* Chân này gồm 2 xương gắn nhau, nên bạn phải di chuyển sau cho khéo 1 tý cho chân hơi cong ở phần đầu gối nhé.

8/ Cái đầu ( Layer 14) Chỉ cần 3 Key
+ Key1: ngả về trước
+ Key2: ngả về sau
+ Key3: Copy Key1

9/ Các bộ phận còn lại, như 2 bàn chân ( 27, 17) sẽ chia 5 key giống 2 chân, bạn cũng chỉnh vị trí sao cho bước đi mềm mại chút
Layer 21, là bắp tay phải, cũng giống tay phải cầm súng
* Thân người (15) hầu như không di chuyển, nhưng bạn cũng chia 5 key, với các vị trí đứng thẳng ( 2 chân thẳng ) thì trọng tâm cao  + Và khi bước đi, thì kéo cả khung xương xuống 1 đoạn, nhớ đặt Ruler ở vị trí bàn trân để căn chỉnh giả làm mặt đất, chỉnh cho chuẩn.

Của mình tạo đây



Nếu bạn làm theo 9 bước nhỏ trên đảm bảo không Animation nào làm khó được bạn, mình cam đoan đấy ( trừ Animation của F.A hoặc FAP thì..... :-))

Sau khi chỉnh, bạn thấy ưng ý thì Export ra thôi, chọn File / Export, để các thông số mặc định, chọn đường dẫn rồi nhấn OK. sau đó Copy toàn bộ các file trong thư mục Export đó quăng vào Resource của game là xong. Có 3 file ( plist, ExportJson, png ).

Không biết mình chỉ dùng Text mà ko có ảnh mỉnh họa, mọi người theo dõi có hiểu không?

Còn bây giờ chúng ta đi vào phần quan trọng không kém trong việc tạo Animation, đó là..

B2 - Import vào game

Bạn tạo 1 Project mới, đặt tên WalkMan nhé

Bạn mở file HelloWorldScene.h, thêm vào đoạn code sau:

Phần Include, bổ sung thêm các thư viện Extensions, CocoStudio, và UI

#include "cocos2d.h"
#include "extensions/cocos-ext.h" 
#include "cocostudio/CocoStudio.h"
#include "UI/CocosGUI.h"

USING_NS_CC;
using namespace cocostudio;

Trong Class:
   
private:
cocostudio::Armature* armature; // Cái này mình chịu ko biết giải thích thế nào, tra từ điển cũng ko có nghĩa nào hợp lý.

Trong file  HelloWorldScene.cpp, Ta thêm code như sau

USING_NS_CC;
using namespace cocostudio;

Trong hàm init ()

// Nạp 3 file tài nguyên được xuất bởi CocoStudio, đoạn này xin ko giải thích thêm, hãy đọc thuộc cú pháp này nhé.

ArmatureDataManager::sharedArmatureDataManager()->addArmatureFileInfo("DemoPlayer0.png","DemoPlayer0.plist","DemoPlayer.ExportJson");
armature = Armature::create("DemoPlayer");
armature->setPosition(ccp(visibleSize.width * 0.8,visibleSize.height * 0.5));
armature->getAnimation()->playByIndex(0);
armature->setScale(0.25);
this->addChild(armature);


Việc nạp tài nguyên từ CocoStudio đơn giản như vậy đó, Nhưng việc build ra không hề đơn giản nhé, mình cũng đã phải loay hay mất nửa ngày để tìm hiểu cách Build. Đã thế việc Build Android, và Win32 lại rất khác nhau. ( WP8 chắc tương tự Win32, còn IOS + MAC thì ko có máy ). Sau đây mình trình bày cách làm cho các bạn nhé.

1/ Build Android 

Vâng, các bạn để ý thấy đoạn Include sau
#include "extensions/cocos-ext.h" 
#include "cocostudio/CocoStudio.h"
#include "UI/CocosGUI.h"

Lệnh Import file này dùng chung cho Cocos2d-x, và để build Android. bạn phải chỉnh sửa file Android.MK nằm trong đường dẫn walkman\proj.android\jni, như sau

Thêm dòng code

Với phiên bản Engine 3.0
LOCAL_WHOLE_STATIC_LIBRARIES += cocostudio_static
LOCAL_WHOLE_STATIC_LIBRARIES += cocos_extension_static
vào bên dưới của
LOCAL_WHOLE_STATIC_LIBRARIES += cocosdenshion_static


$(call import-module,editor-support/cocostudio)
$(call import-module,extensions)

vào dưới lệnh
include $(BUILD_SHARED_LIBRARY)


Với phiên bản Engine 3.1.x, bạn chỉ việc xóa dấu # trước 24dòng
cocostudio_static, 
cocos_extension_static,
$(call import-module,extensions), 
$(call import-module,editor-support/cocostudio)

là ok,

Nếu bạn có thêm Class mới nhớ phải thêm Class vào đoạn này

LOCAL_SRC_FILES := hellocpp/main.cpp \ 


Xong rồi, Build Android thôi, các bạn tự làm nhé, xong rồi quẳng vào Máy ĐT để chạy nhé, ko nên xài máy ảo.

* Có  chút lưu ý, file Android.MK ở 2 phiên bản 3.0 và 3.1.x có 1 chút khác biệt 
+ 3.1.x bổ sung sẵn các mở rộng và đóng dấu #, và có dòng này
$(call import-module,.) khác với bản 3.0 là $(call import-module,2d). Bạn đừng cố sửa đổi thành 2d khi đang xài Engine 3.1.x
+ 3.0 thì cung cấp phần mở rộng ít hơn, và ko có gợi ý trong dấu #, bạn phải có chút kinh nghiệm mới biết thêm gì vào chỗ này khi dùng thêm thư viện.

2/ Build Window

Build cha này phức tạp hơn 1 chút, và nếu có xảy ra lỗi thì quả thật rất khó tìm, liên quan tới code thì không sao, nhưng liên quan tới thư viện thì khó tìm lắm. Mày mò cũng mất khá nhiều thời gian. Và đây là kết quả mày mò của mình sau nửa ngày.

Bạn để ý phần Include

#include "extensions/cocos-ext.h" 
#include "cocostudio/CocoStudio.h"
#include "UI/CocosGUI.h"

Ở trường hợp Android, bạn thêm 2 dòng code vào Android.MK, vậy thì với window bạn phải thêm dòng code tương tự vào đâu. Chính là file walkman.vcxproj ( file VCXPROJ này rất quan trọng nhé, các bạn lưu ý). 

Đầu tiên: 
Bạn hãy xem lại bài 15, cách mình thêm thư viện Box2D vào VS2012 nhé. Ở bài này chúng ta cũng làm tương tự như vậy nhưng ko phải là Box2D mà là 3 thư viện nêu trong INCLUDE kia, quan trọng là bạn phải biết IMPORT đúng thư viện nào, mình chỉ luôn:

+ libExtensions.vcxproj nằm trong đường dẫn walkman\cocos2d\extensions\proj.win32
+ libCocosStudio.vcxproj nằm trong walkman\cocos2d\cocos\editor-support\cocostudio\proj.win32
libGUI.vcxproj nằm trong walkman\cocos2d\cocos\ui\proj.win32

Và kết quả mình thêm đây


Đã có 3 lib kể trên ( Nhớ xem lại cách làm ở bài 15 - QUAN TRỌNG nhé). bạn phải nhớ SAVE solution đấy, sau đó bạn mở file walkman.vcxproj trong đường dẫn ( walkman\proj.win32 ) lên xem kết quả


Đã chứa 3 Lib ( và có mã ID ở dưới, vì thế bạn ko thể Add = tay nhé)

Tiếp theo: 

Các bạn search trong file walkman.vcxproj cụm từ AdditionalIncludeDirectories

và tìm tới dòng 72 + 105 (2 vị trí): thêm vào đoạn code sau ngay phía đầu

$(EngineRoot)cocos;$(EngineRoot)cocos\editor-support;$(EngineRoot);

Hoặc bạn có thể thêm các dòng trên ( EnginRoot) bằng VS2012 như sau

+ Chuột phải vào project walkman trong cửa sổ Solution Explorer / References / Configuration Properties / Additional Include Directories ( Ngay dòng đầu tiên ) có các thông số ở ô bên phải. Bạn Click vào đó và chọn Edit nhé, rồi chọn nút New Line - nút đầu tiên ( hình thư mục ). Add 3 cái EngineRoot vào là Xong

OK, chỉ như vậy thôi, là BUILD thành công, có phức tạp quá không nhỉ ???

Kết quả đây, 1 chú Cowboy đang bước đi tại chỗ, hehe. Nếu mình xây dựng hàm Touch cho move tới vị trí Touch thì sẽ có nhân vật bước đi thôi. Để sau vậy






OK, Vậy là đã xong 1 bài khá công phu. Trong bài này chúng ta học được những gì? Rất nhiều cái hay ho:

+ Sử dụng chức năng Animation của CocoStudio thành thạo.
+ Cách Import tài nguyên vào game qua đoạn code đơn giản.
+ Cách Build với Android, Window khi thêm thư viện mới.

Một số điểm còn hạn chế:

+ Mới học được 1/4 chức năng của CocoStudio thôi nhé, Nhân vật mới chỉ có 1 chuyển động. Vậy nếu nhân vật có nhiều chuyển động thì sao? Import Particle, Vật lý vào thế nào đây? Các bài sau hi vọng sẽ có.

+ Mới chỉ có cách build trên 2 Platform Android, Window, còn Mac + IOS, WP8 thì mình ko có máy MAC + Win8 nên chịu, các bạn mò vào proj.ios_mac, proj.wp8-xaml xem có soi được gì ko.

Mình dừng bài ở đây thôi, dài quá rồi.

Hẹn gặp lại các bạn ở những bài sau.

Dowload Source:

+ Cocostudio Project
+ Class
+ Export Resource

À, vào thời điểm mình viết xong bài này, CocoStudio có phiên bản mới 1.5 nhé. Mọi người nghiên cứu có gì HOT

Bài 25: Hiệu ứng Parallax Scrolling

Thursday, June 19, 2014

Bài 23 - Giới thiệu CocoStudio - Công cụ tạo Animation cực cool (Part 1)

Hi mọi người!

Animation là 1 phần không thể thiếu trong Game. Ở bài trước, chúng ta đã cùng nhau nghiên cứu cách tạo Animation bằng Sprite Sheet. Cách này có ưu điểm là đơn giản dễ làm, tuy nhiên sẽ gặp phải vấn đề về bộ nhớ  nếu khối lượng Sprite Sheet lớn, số lượng ảnh chuyển động phải vẽ là khá nhiều, nếu game mà có nhiều nhân vật chuyển động thì chắc cũng hơi oải nhỉ.

Trong bài này mình sẽ hướng dẫn các bạn Cách sử dụng CocoStudio để tạo Animation. Loại Animation tạo ra bởi CocoStudio có ưu điểm là mềm mại hơn, linh hoạt hơn, giải quyết được vấn đề bộ nhớ, quy trình làm cũng không quá khó. Đó chính là Skeleton Animation - Chuyển động dạng khung xương

Nội dung chủ yếu của bài:

+ Giới thiệu về CocoStudio và các chức năng chủ yếu của nó
+ Làm quen với giao diện thiết kế Animation
+ Các bước thiết kế Animation từ việc: dựng khung xương, kéo thả hình ảnh, gắn xương, tạo chuyển động với keyframe trên timeline

OK, thế thôi nhỉ! À, bài này khiến mình hơi phân vân trong việc

+ Hướng dẫn chi tiết bằng hình ảnh
+ Hay quay video + chú thích với từng bước

Thôi, cứ làm cách 1 đã, Video hẹn hôm sau

B1 - Giới thiệu về Cocos Studio, Các chức năng của nó

CocoStudio - cái tên nói lên tất cả. là phần mềm chuyên để thiết kế giao diện, animation, scene, data cho Engine Cocos2d-x. Soft nào mà gắn chữ Studio là bạn biết rồi đấy, chuyên để design.

Bạn hoàn toàn có thể chỉ dùng mỗi Engine Cocos2d-x để build game với từng dòng Code. Tuy nhiên nhờ sự hỗ trợ mạnh mẽ của các extension, plugin hoặc các soft của bên thứ 3 như CocoStudio mà việc thiết kế Game của bạn đơn giản đi rất nhiều. Tất nhiên nó sẽ không làm cho việc thiết kế game đơn giản tới mức chỉ việc ngồi rung đùi, uống cà phê và kéo thả đâu. Nó chỉ làm đơn giản hóa 1 vài công đoạn viết code cho bạn thôi. Tất nhiên đều là những công đoạn quan trọng cả.

Giao diện chính


Những tính năng chủ yếu của CocoStudio

+ Thiết kế Animation
+ Thiết kế Giao diện
+ Thiết kế Màn chơi
+ Thiết kế Data

Bạn có thể download bản mới nhất tại trang chủ của Cocos2d-x. Hiện phiên bản mới nhất là 1.4.x, hỗ trợ Engine 3.1.1

B2 - Làm quen với giao diện thiết kế Animation

Các bạn mở CocoStudio lên, chọn nút đầu tiên : Animation Editor

Giao diện thiết kế của phần Animation như ảnh dưới


Chọn New Project ở bên trái, trọn tên và vị trí lưu Project



Và dưới đây là toàn bộ giao diện WorkSpace của CocoStudio 1.4.x,

Chế độ Pose Mode ( dựng xương, và hình )
















Chế độ Aniatmion : Dựng hoạt hình






Sau đây sẽ là các lệnh, và nút lệnh cơ bản CocoStudio. Thực sự khi mới bắt đầu dùng thì mình cũng thấy hơi khó khăn, do chưa quen nút lệnh, menu chuột phải thì rất ít tác vụ. Nhưng chỉ sau khoảng 1h đồng hồ tìm hiểu là các bạn biết cách sử dụng thành thạo phần Animation này ngay ấy mà. Mình chắc chắn đấy.

* Các menu chính

FILE:


EDIT


VIEW


WINDOW


2 Menu Help, Language thì thôi nhé. Các menu kể trên rất gọn gàng và đơn giản phải không.

* Các Nút lệnh




Các nút màu đỏ là khá quan trọng, thường hay dùng nhé, 2 chế độ Pose Mode và Animation đều dùng chung các nút này.

Quên 1 nút chưa chú thích là nút Selection hình mũi tên và dấu +

* Các khung cửa sổ cơ bản



Cửa sổ Action và Timeline chỉ xuất hiện ở Mode Animation nhé

Cửa sổ ẩn, Bones Relationship ( quan hệ xương ), trông khá giống cây gia phả nhỉ, Xương "cha" nằm phía trên, xương "con" nằm phía dưới, khi di chuyển xương "Cha", xương "con" sẽ di chuyển theo






Cửa sổ Resource Editor ( Sửa tài nguyên ), ở đây bạn có thể chỉnh vị trí điểm neo ( dấu + đỏ ) là điểm gắn vào xương. Còn mấy cái vẽ khung hình ( vẽ ra khung màu xanh ) trong này mình cũng chưa biết ý nghĩa để làm gì nữa.



Xong bước làm quen với Giao diện, Work Space của CocoStudio rồi, chúng ta sẽ đi vào 1 phần rất hay của bài hôm nay, đó là tạo Dựng khung Xương

B3: Thiết kế Animation qua từng bước

1/ Dựng khung xương

Mở khung làm việc, chọn Create Bones ( Alt + K ), vẽ 1 bộ khung xương người như sau, chỉ việc kéo chuột từ điểm này tới điểm kia thôi, khi cần di chuyển xương, nhấn Alt + S, khi cần xoay xương chọn nút "Cử Động Xương". Cần kéo dài, hoặc rút ngắn xương hãy chọn Alt + D. Trông khá giống khung xương người nhỉ



2/ Liên kết khung xương với nhau, mục đích để chúng chuyển động hài hòa, ăn ý với nhau gần giống thật nhất
( Ví dụ: liên kết 2 xương tay, 2 xương chân, liên kết chân với người, đầu với người ) Càng nhiều liên kết thì chuyển động càng uyển chuyển, nhưng cũng khó quản lý hơn đấy

Có 3 cách để liên kết khung xương
C1 : Sử dụng menu chuột phải, chọn Binding Parent ( Alt +P ) : bạn Click chọn 1 Xương - mục đích làm xương con ( nó sáng màu ) sau đó chuột phải chọn Binding Parent ( hoặc nhấn Alt +P ), sau đó chọn 1 xương Cha, 1 thông báo Binding thành không sẽ hiện ra, như sau


C2 : Sử dụng bảng quan hệ Bones RelationShip, với cách này bạn làm như sau: Kéo xương con vào xương cha là xong, mối quan hệ cha con sẽ được thể hiện qua mũi tên, đầu nhọn mũi tên là con, đầu ko có mũi tên là cha
C3 : Sử dụng nút "Gắn xương" để chuyển sang Binding Mode, rồi kéo xương Con vào xương Cha

Mỗi cách có 1 điểm mạnh điểm yếu riêng:

- Cách 1 nhìn được vị trí các xương, có thể xác định được xương nào với xương nào, nhưng ko thể nhìn thấy được mối quan hệ giữa các xương, ko biết xương cha con,
- Cách 2 không nhìn thấy vị trí các xương, vẫn có thể xác định được xương nào = cách click vào layer, nhìn thấy được mối quan hệ cha con.
- Cách 3: nhìn được vị trí các xương, không nhìn thấy mối quan hệ, nhưng có khả năng kéo thả tiện dụng

Hãy phối hợp cả 3 cách để xây dựng khung xương linh hoạt.

* Lưu ý khi tạo liên kết xương:

+ 1 Con không thể có >1 Cha, Khi bạn tạo cha mới cho 1 xương con đã có cha, thì Xương con sẽ nhận Xương Cha sau cùng
+ 1 Cha có thể có nhiều con
+ 1 Xương con, lại có thể là Cha của 1 hoặc nhiều xương con khác

+ Bạn không đảo liên kết Cha - Con, Ông - Cháu với nhau khi còn liên kết Ông - Cha - Con. Tức là bạn ko thể thiết lập cho Cháu lại làm "bố" của Ông, và Con ko thể làm "bố" của Cha. Giống với thực tế đấy chứ. Nhưng khi bạn hủy bỏ liên kết Ông - Cha - Con đi, thì bạn hoàn toàn có thể chọn xương nào làm Cha, xương nào làm Con cũng được, nhưng phải tuân thủ 3 điều trên

Hãy nắm vững 3 quy tắc đó là bạn sẽ dựng được khung xương dễ dàng

Đây là bảng khung xương mình đã dựng ( hình của bạn có thể khác )











OK giờ sang bước tiếp theo là, gắn "da thịt" cho bộ xương này. Có bạn sẽ thắc mắc, lấy "da thịt" này ở đâu? Da thịt ở đây chính là hình ảnh của nhân vật bạn định tạo ra trong game, hình ảnh nhân vật thì bạn phải vẽ rồi. Khi vẽ nhân vật bạn hãy hình dung ra nhân vật sẽ cử động như thế nào, càng nhiều cử động trên cơ thể thì khung xương càng phức tạp. Đơn giản thì chia cử động nhân vật thành các phần: Đầu, thân, tay, chân, bàn chân, bàn tay, vũ khí..... Khi có nhân vật tổng thể bạn phân chia hình ảnh ra các phần Đầu, thân, tay, chân, bàn chân, bàn tay, vũ khí....có thể mỗi phần chia sẽ lẹm của nhau một tý nhưng ko sao đâu, làm thế nào khi ghép lại trông không quá rời rạc là được.

Ở bài này mình lấy sẵn thư viện của CocoStudio để minh họa nhé, tất nhiên chỉ lấy phần tài nguyên hình ảnh thôi.

Nhân tiện về phần Tài nguyên của CocoStudio mình giới thiệu luôn là, nó có thể Import các loại tài nguyên sau đây:

+ Hình ảnh
+ File Json
+ File Plist
+ File Fla?? ( Chưa thử )

Rất phong phú phải không, hình ảnh thì bình thường rồi, nhưng 3 cái sau rất quan trọng nhé, JSon, Plist có thể dùng cho Particle, Physic, tuyệt không?

* Đắp da thịt cho Xương
+ Trước tiên bạn Import Tài nguyên vào bằng lệnh Import Folder / hoặc Import Files ( Chuột phải ở ô Resource ), rồi chọn Tài nguyên ( Image, Json, Plist, Fla)


Tiếp theo là bước quan trọng, đắp da, thịt cho nhân vật, có 2 cách như sau

+ Cách 1: Kéo thả từng ảnh vào WorkSpace : ví dụ ảnh cái đầu ( bạn có thể phóng to thu nhỏ ảnh cho phù hợp khung hình. Sau đó chuột phải vào Ảnh, chọn Binding Bones( Alt + I) rồi tiếp theo chọn cái Xương đầu là xong. Một khi đã gắn Thịt vào Xương, bạn không thể tách ra được, không còn menu chuột phải trên hình ảnh, bạn chỉ có thể xóa Xương đó khỏi khung xương, hoặc Ctr + Z

Để kiểm tra Ảnh đã được gắn vào Xương chưa, chọn nút Xoay Xương, nếu thấy ảnh xoay theo thì là thành công

+ Cách 2: Bạn chọn vào Xương cần gắn, nhìn vào bảng bên phải, ở Tab Layer, xương nào được chọn sẽ sáng lên, tiếp đó bạn Click vào Tab Properties. Tab này chứa các thông số của Xương như: Tên, chiều dài, góc quay, tọa độ, GRAPHIC. bạn có thể dùng các thông số này để hiệu chỉnh Xương khi chưa gắn Ảnh, hoặc hiệu chỉnh cả Ảnh khi được gắn vào Xương. ( Nếu không nhìn thấy 2 Tab này hãy vào menu Window/Reset Window Layout)



Bạn để ý 1 ô quan trọng, GRAPHIC, hãy kéo hình ảnh vào đây, nó sẽ gắn luôn với Xương được chọn, nhưng hình ảnh sẽ không đúng tỷ lệ mong muốn, và thường bị quay đi 1 góc. Hãy chỉnh lại tỷ lệ ảnh, và góc quay với Lưu ý bên dưới.

* Lưu ý đặc biệt quan trọng

2 Cách đều có cái hay riêng:

Cách 1: Kéo thả ảnh, chỉnh ảnh - xoay ảnh thoải mái rồi mới gắn xương. Tuy nhiên có thể gắn nhầm xương, hoặc chọn nhầm ảnh ( do nhiều ảnh ) để gắn.

Cách 2: Khó để nhầm ảnh hoặc Xương, gắn phát dính luôn,

Và cái dở:

Cách 1: Bạn không thể tách xương khỏi ảnh, tuy nhiên vẫn có thể dùng Ctrl + Z để trở lại bước chưa gắn xương

Cách 2: Bạn cũng ko thể tách xương khỏi ảnh, và Đặc Biệt, bạn ko thể dùng Ctrl + Z để trở lại bước trước khi gắn Xương, nghĩa là bạn chỉ có thể mở lại Project ( nếu chưa lưu bước gắn xương mà thôi ), hoặc chỉ có thể xóa Xương và Ảnh khỏi khung xương. Sẽ rắc rối nếu Xương đó là xương Cha của nhiều xương, lại phải gắn khung xương lại rồi.

Và có 1 điều khó chịu khi làm theo cách 2  ( thậm chí là cách 1) . Khi gắn xương theo cách 2, ảnh sẽ bị quay 1 góc, bạn phải làm như sau:
- Trước tiên Bạn phải chỉnh tỷ lệ hình ảnh = 4 nút vuông ở 4 góc ảnh, nhớ giữ SHIFT
- Sau đó bạn mới quay ảnh = 1 nút vuông nằm trên 1 Cạnh của ảnh
- Nếu ko làm theo thứ tự này, tức bạn xoay ảnh trước, rồi mới chỉnh tỷ lệ, thì sẽ cực khó chịu vì phương chiều sẽ bị đảo lộn rất ức chế. Bạn thử đi là biết.

Vậy mình khuyên các bạn dùng cách 1 đi, bớt khó chịu hơn cách 2

Và đây là khung xương đã gắn da thịt của mình ( Mình lấy tạm Project của CocoStudio nhé )






Mình dừng bài học ở đây nhé, cũng khá dài rồi. Phần Animation mình hẹn ở Phần 2 nha. Trong bài này chúng ta đã học được:

+ Các chức năng chính của CocoStudio
+ Làm quen giao diện WorkSpace
+ Dựng khung xương, gắn xương với nhau
+ Đắp "da thịt" cho bộ xương

Chào và hẹn gặp lại các bạn ở bài sau!

Bài 24 - Giới thiệu CocoStudio - Công cụ tạo Animation cực cool (Part 2)

Friday, June 13, 2014

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

Hi mọi người!

Cả tuần nay bận quá chả viết được bài. Nhưng mình chắc chắn rằng sau khi đọc xong 21 bài TUT trong này, các bạn có thể tự đọc các bài Tut khác trên mạng tại những trang nước ngoài rồi đó. Có mấy bạn đã Build xong game Sushi này rồi đấy, Hi. Nay mình sẽ hướng dẫn nốt cho những người chưa xong game này nha.

Qua 2 bài trước chúng ta đã từng bước học được cách làm game Sushi Crush giống game Candy Crush nổi tiếng nhé. Tuy nhiên bạn vẫn chưa thể di chuyển các Sushi, cũng như ăn các chuỗi Sushi được. Ở trong bài này, chúng ta sẽ cùng nhau đi nốt phần còn lại của game này, với các công việc cụ thể sau đây:

+ Xây dựng mode chơi, Bổ sung thêm các loại Sushi đặc biệt
+ Di chuyển Sushi nhờ sự kiện Touch
+ Tạo các hiệu ứng nổ đặc biệt khi ăn các Sushi đặc biệt

Công việc chỉ có vậy thôi, nhưng chắc code sẽ hơi dài nên có thể mình sẽ chia bài này ra làm 2 phần để mọi người tiện theo dõi.

Bắt đầu nhé! Trình bày nhiều quá.

B1 - Thêm mode chơi, bổ sung thêm các loại Sushi đặc biệt

Các bạn mởi file SushiSprite.h, ta khai báo thêm 1 loại dữ liệu sau, ngay trước Class nhé

typedef enum {
    DISPLAY_MODE_NORMAL = 0,
    DISPLAY_MODE_HORIZONTAL,
    DISPLAY_MODE_VERTICAL,
} DisplayMode;

Và trong phần Public của Class SushiSprite, thêm đoạn code sau

    CC_SYNTHESIZE(bool, m_isNeedRemove, IsNeedRemove); // Cờ đánh dấu cần loại bỏ
    CC_SYNTHESIZE(bool, m_ignoreCheck, IgnoreCheck); //  Cờ bỏ qua kiểm tra
    CC_SYNTHESIZE_READONLY(DisplayMode, m_displayMode, DisplayMode); // Mode hiển thị
    void setDisplayMode(DisplayMode mode); // Thiết lập mode

Các bạn mởi file SushiSprite.cpp, ta khai báo thêm 2 mảng SushiSprite nữa cho các loại Sushi đặc biệt ( Sọc dọc, Sọc ngang)

static const char *sushiVertical[TOTAL_SUSHI] = {
"sushi_1v.png",
"sushi_2v.png",
"sushi_3v.png",
"sushi_4v.png",
"sushi_5v.png",
"sushi_6v.png"
};

static const char *sushiHorizontal[TOTAL_SUSHI] = {
"sushi_1h.png",
"sushi_2h.png",
"sushi_3h.png",
"sushi_4h.png",
"sushi_5h.png",
"sushi_6h.png"
};

Hàm tạo thay đổi lại 1 chút ( do có thêm biến mới)

SushiSprite::SushiSprite()
: m_col(0)
, m_row(0)
, m_imgIndex(0)
, m_isNeedRemove(false)
, m_ignoreCheck(false)
, m_displayMode(DISPLAY_MODE_NORMAL)
{
}

Xây dựng hàm setDisplayMode

void SushiSprite::setDisplayMode(DisplayMode mode)
{
    m_displayMode = mode;
    
    SpriteFrame *frame;

    // Tùy theo các trường hợp của mode, mà tạo ra loại Sushi tương ứng mode đó
    switch (mode) {
        case DISPLAY_MODE_VERTICAL:
            frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(sushiVertical[m_imgIndex]);
            break;
        case DISPLAY_MODE_HORIZONTAL:
            frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(sushiHorizontal[m_imgIndex]);
            break;
        default:
            return;
    }
    setDisplayFrame(frame); // Hàm của lớp Sprite
}

Lớp SushiSprite chỉ có thêm 1 số bổ sung nhỏ vậy thôi, bây giờ chúng ta sẽ đi vào lớp chính của game là lớp PlayLayer

B2 - Di chuyển Sushi bằng Sự kiện Touch

Bạn mở file PlayLayer.h, bổ sung thêm code như sau

Phần Public:

    virtual bool onTouchBegan(Touch *touch, Event *unused) override;
    virtual void onTouchMoved(Touch *touch, Event *unused) override;

Phần Private:

    bool m_isTouchEnable; // Cờ cho phép Touch hoặc ko
    SushiSprite *m_srcSushi; // Pointer: Sushi nguồn
    SushiSprite *m_destSushi; // Pointer: Sushi đích
    bool m_isNeedFillVacancies; // Cờ điền đầy khoảng trống
    bool m_movingVertical; // Cờ di chuyển theo chiều dọc

    void actionEndCallback(Node *node);     // Dừng Action ?
    void explodeSpecialH(Point point); // Nổ theo chiều ngang
    void explodeSpecialV(Point point); // Nổ theo chiều dọc
    SushiSprite *sushiOfPoint(Point *point); // Sushi ở vị trí tọa độ Point
    void swapSushi(); // Đảo 2 Sushi
    void markRemove(SushiSprite *sushi); // Đánh dấu loại bỏ

Bạn mở file PlayLayer.cpp, bổ sung thêm code như sau

+ Sửa lại hàm tạo ( do thêm thuộc tính mới )

PlayLayer::PlayLayer()
: spriteSheet(NULL)
, m_matrix(NULL)
, m_width(0)
, m_height(0)
, m_matrixLeftBottomX(0)
, m_matrixLeftBottomY(0)
, m_isNeedFillVacancies(false)
, m_isAnimationing(true) // Đặt cờ cho Animate
, m_isTouchEnable(true) // Cho phép Touch
, m_srcSushi(NULL)
, m_destSushi(NULL)
, m_movingVertical(true)  // Rơi Sushi
{
}

+ Trong hàm init() thêm code bắt sự kiện Touch

    auto touchListener = EventListenerTouchOneByOne::create();
    touchListener->onTouchBegan = CC_CALLBACK_2(PlayLayer::onTouchBegan, this);
    touchListener->onTouchMoved = CC_CALLBACK_2(PlayLayer::onTouchMoved, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);

+ Dựng hàm phụ , trả về SushiSprite tại 1 vị trí Point

SushiSprite *PlayLayer::sushiOfPoint(Point *point)
{
    SushiSprite *sushi = NULL;
    Rect rect = Rect(0, 0, 0, 0); // Hình chữ nhật kích thước 0,0 tại Point 0,0
    
    // Duyệt ma trận Sushi
    for (int i = 0; i < m_height * m_width; i++) {
        sushi = m_matrix[i];

        // Tính kích thước hình chữ nhật bao quanh Sushi
        if (sushi) {
            rect.origin.x = sushi->getPositionX() - (sushi->getContentSize().width / 2);
            rect.origin.y = sushi->getPositionY() - (sushi->getContentSize().height / 2);
            rect.size = sushi->getContentSize();

            // Nếu hình chữ nhật đó chứa Point ( chắc là point của điểm Touch )
            if (rect.containsPoint(*point)) {
                return sushi; // trả lại Sushi
            }
        }
    }
 
    return NULL; // Trả lại Null nếu Touch ra ngoài ma trận, điểm Touch ko thuộc 1 Sushi nào
}

+ Hàm bắt sự kiện Touch

bool PlayLayer::onTouchBegan(Touch *touch, Event *unused)
{
    m_srcSushi = NULL; // Sushi nguồn
    m_destSushi = NULL; // Sushi dích, dùng để Swap cho nhau
    if (m_isTouchEnable) { // cho phép Touch, khi chưa ăn thì cho phép Touch
        auto location = touch->getLocation(); // lấy điểm Touch
        m_srcSushi = sushiOfPoint(&location); // Trả về Sushi tại điểm Touch
    }
    return m_isTouchEnable;
}

// Di chuyển Sushi
void PlayLayer::onTouchMoved(Touch *touch, Event *unused)
{
    if (!m_srcSushi || !m_isTouchEnable) { // Nếu Touch ra ngoài ( ko chứa Sushi nào ) hoặc ko được phép Touch
        return;
    }
 
    // Lấy vị trí Row, Col của Sushi của Sushi nguồn
    int row = m_srcSushi->getRow();
    int col = m_srcSushi->getCol();
 
    auto location = touch->getLocation();

    // 1/2 Chiều rộng và 1/2 chiều cao
    auto halfSushiWidth = m_srcSushi->getContentSize().width / 2;
    auto halfSushiHeight = m_srcSushi->getContentSize().height / 2;
 

    // Hướng di chuyển

    // Khung chữ nhật "phía trên Sushi nguồn"
    auto  upRect = Rect(m_srcSushi->getPositionX() - halfSushiWidth,
                        m_srcSushi->getPositionY() + halfSushiHeight,
                        m_srcSushi->getContentSize().width,
                        m_srcSushi->getContentSize().height);
 
    // Nếu khung này chứa điểm Touch, nghĩa là ta sẽ di chuyển 1 Sushi đi lên trên
    if (upRect.containsPoint(location)) {
        row++; // Hàng trên của Sushi Nguồn
        if (row < m_height) {
            m_destSushi = m_matrix[row * m_width + col]; // Lấy Sushi đích
        }
        m_movingVertical = true; // Di chuyển dọc = true
        swapSushi(); // Đảo 2 Sushi nguồn và đích cho ngau
        return; // Kết thúc hàm
    }
 
    // Khung chữ nhật "phía dưới Sushi nguồn", vì sao có halfSushiHeight * 3, bạn hãy vẽ hình ra cho dễ hình dung là nhớ là tọa độ gốc của hình Rectang là điểm Left - Bottom nhé, chiều cao + rộng sẽ dựng lên theo trục X ( sang phải ), và trục Y ( lên trên ). OK??
    auto  downRect = Rect(m_srcSushi->getPositionX() - halfSushiWidth,
                        m_srcSushi->getPositionY() - (halfSushiHeight * 3),
                        m_srcSushi->getContentSize().width,
                        m_srcSushi->getContentSize().height);

   // Chứa Touch
    if (downRect.containsPoint(location)) {
        row--; // Hàng dưới
        if (row >= 0) {
            m_destSushi = m_matrix[row * m_width + col];
        }
        m_movingVertical = true;
        swapSushi();
        return;
    }
 
    // Các bước di chuyển sang trái, sang phải, ở đoạn code bên dưới cũng giải thích như trên các bạn nhé
    auto  leftRect = Rect(m_srcSushi->getPositionX() - (halfSushiWidth * 3),
                          m_srcSushi->getPositionY() - halfSushiHeight,
                          m_srcSushi->getContentSize().width,
                          m_srcSushi->getContentSize().height);
 
    if (leftRect.containsPoint(location)) {
        col--;
        if (col >= 0) {
            m_destSushi = m_matrix[row * m_width + col];
        }
        m_movingVertical = false;
        swapSushi();
        return;
    }
 
    auto  rightRect = Rect(m_srcSushi->getPositionX() + halfSushiWidth,
                          m_srcSushi->getPositionY() - halfSushiHeight,
                          m_srcSushi->getContentSize().width,
                          m_srcSushi->getContentSize().height);
 
    if (rightRect.containsPoint(location)) {
        col++;
        if (col < m_width) {
            m_destSushi = m_matrix[row * m_width + col];
        }
        m_movingVertical = false;
        swapSushi();
        return;
    }

}


+ Hàm đảo 2 Sushi

void PlayLayer::swapSushi()
{
    m_isAnimationing = true; // cho phép Animation
    m_isTouchEnable = false; // Dừng Touch

    if (!m_srcSushi || !m_destSushi) { // Ko tồn tại 1 trong 2 Sushi để đảo nhau
        m_movingVertical = true;
        return;
    }
    // Lấy tọa độ Point của 2 loại Sushi được đảo
    Point posOfSrc = m_srcSushi->getPosition();
    Point posOfDest = m_destSushi->getPosition();
    float time = 0.2;
 
    // 1.Hoán vị hàng, cột 2 Sushi trong ma trận, tham số quan trọng nhất là Row và Col của Sushi
    m_matrix[m_srcSushi->getRow() * m_width + m_srcSushi->getCol()] = m_destSushi;
    m_matrix[m_destSushi->getRow() * m_width + m_destSushi->getCol()] = m_srcSushi;
    int tmpRow = m_srcSushi->getRow();
    int tmpCol = m_srcSushi->getCol();
    m_srcSushi->setRow(m_destSushi->getRow());
    m_srcSushi->setCol(m_destSushi->getCol());
    m_destSushi->setRow(tmpRow);
    m_destSushi->setCol(tmpCol);
 
    // 2.Kiểm tra xem có dãy >= 3 Sushi giống nhau được tạo ra bởi 2 Sushi sau hoán đổi này ko
    std::list<SushiSprite *> colChainListOfFirst;
    getColChain(m_srcSushi, colChainListOfFirst);
 
    std::list<SushiSprite *> rowChainListOfFirst;
    getRowChain(m_srcSushi, rowChainListOfFirst);
 
    std::list<SushiSprite *> colChainListOfSecond;
    getColChain(m_destSushi, colChainListOfSecond);
 
    std::list<SushiSprite *> rowChainListOfSecond;
    getRowChain(m_destSushi, rowChainListOfSecond);
 
    if (colChainListOfFirst.size() >= 3
        || rowChainListOfFirst.size() >= 3
        || colChainListOfSecond.size() >= 3
        || rowChainListOfSecond.size() >= 3) {

        // Animation đảo vị trí cho nhau
        m_srcSushi->runAction(MoveTo::create(time, posOfDest));
        m_destSushi->runAction(MoveTo::create(time, posOfSrc));
        return;
    }
 
    // 3.Không tạo được chuỗi, Đảo trở lại vị trí cũ
    m_matrix[m_srcSushi->getRow() * m_width + m_srcSushi->getCol()] = m_destSushi;
    m_matrix[m_destSushi->getRow() * m_width + m_destSushi->getCol()] = m_srcSushi;
    tmpRow = m_srcSushi->getRow();
    tmpCol = m_srcSushi->getCol();
    m_srcSushi->setRow(m_destSushi->getRow());
    m_srcSushi->setCol(m_destSushi->getCol());
    m_destSushi->setRow(tmpRow);
    m_destSushi->setCol(tmpCol);
 
    // Di chuyển 2 bước, đảo vị trí, rồi trở lại vị trí cũ
    m_srcSushi->runAction(Sequence::create(
                                      MoveTo::create(time, posOfDest),
                                      MoveTo::create(time, posOfSrc),
                                      NULL));
    m_destSushi->runAction(Sequence::create(
                                      MoveTo::create(time, posOfSrc),
                                      MoveTo::create(time, posOfDest),
                                      NULL));
}


B3 - Tạo các hiệu ứng nổ đặc biệt

Trước hết ta cần sửa đổi lại một sô hàm sau

+ Hàm Update

Thay đoạn code

   if (!m_isAnimationing) {
        checkAndRemoveChain();
    }

Thành =>

    // Thiết lập cờ cho phép Touch khi không còn chuyển động, và ngược lại
    m_isTouchEnable = !m_isAnimationing;
 
    //Nếu ko có chuyển động
    if (!m_isAnimationing) {
        // Xét xem phải điền đầy ô trống không
        if (m_isNeedFillVacancies) {
            fillVacancies(); // điền đầy
            m_isNeedFillVacancies = false;
        } else {
            checkAndRemoveChain(); // Kiểm tra và ăn các chuỗi
        }
    }

+ Hàm getColChain, và getRowChain bạn bổ sung code như sau

Tìm các đoạn code

if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex()))

Sửa thành =>

        // Tồn tại Sushi đứng cạnh : cùng loại + chưa gắn cờ Remove và cờ Ignorecheck
        if (neighborSushi
            && (neighborSushi->getImgIndex() == sushi->getImgIndex())
            && !neighborSushi->getIsNeedRemove()
            && !neighborSushi->getIgnoreCheck())

+ Hàm fillVacancies, bổ sung thêm 

    // Cho phép Animation và rơi
    m_movingVertical = true;
    m_isAnimationing = true;

+ Hàm removeSushi, loại bỏ tham số truyền, các bạn nhớ sửa trong PlayLayer.h

void PlayLayer::removeSushi() // Không cần truyền tham số
{

    m_isAnimationing = true;
 
    // Duyệt toàn ma trận
    for (int i = 0; i < m_height * m_width; i++) {
        SushiSprite *sushi = m_matrix[i];
        if (!sushi) { // Bỏ qua Sushi rỗng
            continue;
        }
     
        if (sushi->getIsNeedRemove()) { // Sushi cần xóa bỏ
            m_isNeedFillVacancies = true; // Cần điền đầy
         
           // Nổ các Sushi đặc biệt
            if(sushi->getDisplayMode() == DISPLAY_MODE_HORIZONTAL) // Loại Sushi sọc ngang
            {
                explodeSpecialH(sushi->getPosition()); // Gọi hàm nổ theo chiều ngang
            }
            else if (sushi->getDisplayMode() == DISPLAY_MODE_VERTICAL) // Loại Sushi sọc dọc
            {
                explodeSpecialV(sushi->getPosition()); // Gọi hàm nổ theo chiều dọc
            }

            explodeSushi(sushi); // Nổ sushi bình thường
         
        }
    }

}

+ Hàm explodeSushi, sửa 1 chút

    sushi->runAction(Sequence::create(
                                      ScaleTo::create(time, 0.0),
                                      CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, sushi)),

                                      NULL));


Thành

    sushi->runAction(Sequence::create(
                                      ScaleTo::create(time, 0.0),
                                      CallFuncN::create(CC_CALLBACK_1(PlayLayer::actionEndCallback, this)),

                                      NULL));


+ Hàm actionEndCallback như này

void PlayLayer::actionEndCallback(Node *node)
{
// Loại bỏ Sushi khỏi ma trận và Layer
    SushiSprite *sushi = (SushiSprite *)node;
    m_matrix[sushi->getRow() * m_width + sushi->getCol()] = NULL;
    sushi->removeFromParent();
}


+ 2 Hàm tạo hiệu ứng nổ đặc biệt theo chiều ngang và dọc

// Nổ theo chiều ngangvoid PlayLayer::explodeSpecialH(Point point)
{
    Size size = Director::getInstance()->getWinSize();

// Tham số để tạo hiệu ứng thôi
    float scaleX = 4 ;
    float scaleY = 0.7 ;
    float time = 0.3;
    Point startPosition = point; // điểm đầu
    float speed = 0.6f;
    
    auto colorSpriteRight = Sprite::create("colorHRight.png");
addChild(colorSpriteRight, 10);
    Point endPosition1 = Point(point.x - size.width, point.y); // Điểm cuối
    colorSpriteRight->setPosition(startPosition);

    // Chỗ này thực hiện 3 hành động, kéo dãn theo X + co lại theo Y, - >chạy sang trái -> xóa khỏi layer
    colorSpriteRight->runAction(Sequence::create(ScaleTo::create(time, scaleX, scaleY),
                                             MoveTo::create(speed, endPosition1),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, colorSpriteRight)),
                                             NULL));
    
    // Giải thích như trên
    auto colorSpriteLeft = Sprite::create("colorHLeft.png");
addChild(colorSpriteLeft, 10);
    Point endPosition2 = Point(point.x + size.width, point.y);
    colorSpriteLeft->setPosition(startPosition);
    colorSpriteLeft->runAction(Sequence::create(ScaleTo::create(time, scaleX, scaleY),
                                             MoveTo::create(speed, endPosition2),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, colorSpriteLeft)),
                                             NULL));
    

}

// Giống hệt hàm explodeSpecialH, chỉ thay đổi phương X thành Y, quá dễ hiểu
void PlayLayer::explodeSpecialV(Point point)
{
    Size size = Director::getInstance()->getWinSize();
    float scaleY = 4 ;
    float scaleX = 0.7 ;
    float time = 0.3;
    Point startPosition = point;
    float speed = 0.6f;

    auto colorSpriteDown = Sprite::create("colorVDown.png");
addChild(colorSpriteDown, 10);
    Point endPosition1 = Point(point.x , point.y - size.height);
    colorSpriteDown->setPosition(startPosition);
    colorSpriteDown->runAction(Sequence::create(ScaleTo::create(time, scaleX, scaleY),
                                             MoveTo::create(speed, endPosition1),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, colorSpriteDown)),
                                             NULL));
    
    auto colorSpriteUp = Sprite::create("colorVUp.png");
addChild(colorSpriteUp, 10);
    Point endPosition2 = Point(point.x , point.y + size.height);
    colorSpriteUp->setPosition(startPosition);
    colorSpriteUp->runAction(Sequence::create(ScaleTo::create(time, scaleX, scaleY),
                                             MoveTo::create(speed, endPosition2),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, colorSpriteUp)),
                                             NULL));
}

Xong các bước chính, giờ chúng ta chỉnh sửa thêm và chỉnh sửa nốt 1-2 hàm nữa là chạy được

+ Hàm markRemove

void PlayLayer::markRemove(SushiSprite *sushi)
{
    if (sushi->getIsNeedRemove()) {
        return;
    }
    if (sushi->getIgnoreCheck()) {
        return;
    }
    
    // Set true
    sushi->setIsNeedRemove(true);
    // Các sushi loại sọc dọc
    if (sushi->getDisplayMode() == DISPLAY_MODE_VERTICAL) {
        for (int row = 0; row < m_height; row++) {
            SushiSprite *tmp = m_matrix[row * m_width + sushi->getCol()];
            if (!tmp || tmp == sushi) {
                continue; //Bỏ qua loại sọc dọc
            }
            
            if (tmp->getDisplayMode() == DISPLAY_MODE_NORMAL) {
                tmp->setIsNeedRemove(true); // Đánh dấu loại Sushi thường
            } else {
                markRemove(tmp); // Đệ quy,
            }
        }
    // Các sushi loại sọc ngang, tương tự
    } else if (sushi->getDisplayMode() == DISPLAY_MODE_HORIZONTAL) {
        for (int col = 0; col < m_width; col++) {
            SushiSprite *tmp = m_matrix[sushi->getRow() * m_width + col];
            if (!tmp || tmp == sushi) {
                continue;
            }
            
            if (tmp->getDisplayMode() == DISPLAY_MODE_NORMAL) {
                tmp->setIsNeedRemove(true);
            } else {
                markRemove(tmp);
            }
        }
    }
}

+ Sửa lại hàm Ăn Sushi checkAndRemoveChain

void PlayLayer::checkAndRemoveChain()
{
    SushiSprite *sushi;
    // Thiết lập cờ IgnoreCheck = false
    for (int i = 0; i < m_height * m_width; i++) {
        sushi = m_matrix[i];
        if (!sushi) {
            continue;
        }
        sushi->setIgnoreCheck(false);
    }
    
    // 2. Kiểm lại
    for (int i = 0; i < m_height * m_width; i++) {
        sushi = m_matrix[i];
        if (!sushi) {
            continue;
        }
        
        if (sushi->getIsNeedRemove()) {
            continue; // Bỏ qua Sushi đã gắn cờ "cần loại bỏ"
        }
        if (sushi->getIgnoreCheck()) {
            continue; // Bỏ qua Sushi đã gắn cờ "bỏ qua kiểm tra"
        }
        
        // Đếm cuỗi
        std::list<SushiSprite *> colChainList;
        getColChain(sushi, colChainList);
        
        std::list<SushiSprite *> rowChainList;
        getRowChain(sushi, rowChainList);
        
        std::list<SushiSprite *> &longerList = colChainList.size() > rowChainList.size() ? colChainList : rowChainList;
        if (longerList.size() < 3) {
            continue;// Bỏ qua
        }
        
        std::list<SushiSprite *>::iterator itList;
        bool isSetedIgnoreCheck = false;
        for (itList = longerList.begin(); itList != longerList.end(); itList++) {
            sushi = (SushiSprite *)*itList;
            if (!sushi) {
                continue;
            }
            
            if (longerList.size() > 3) {
                // Sushi đặc biệt khi chuỗi có 4 hoặc 5 Sushi
                if (sushi == m_srcSushi || sushi == m_destSushi) {
                    isSetedIgnoreCheck = true;
                    sushi->setIgnoreCheck(true);
                    sushi->setIsNeedRemove(false);

// Tùy theo hướng di chuyển mà tạo ra loại Sushi sọc dọc hay ngang
                    sushi->setDisplayMode(m_movingVertical ? DISPLAY_MODE_VERTICAL : DISPLAY_MODE_HORIZONTAL);
                    continue;
                }
            }
            
            markRemove(sushi); // Đánh dấu cần loại bỏ sushi
        }
        
        // Chuỗi đặc biệt, khi Sushi rơi, sinh ra tự nhiên
        if (!isSetedIgnoreCheck && longerList.size() > 3) {
            sushi->setIgnoreCheck(true);
            sushi->setIsNeedRemove(false);
            sushi->setDisplayMode(m_movingVertical ? DISPLAY_MODE_VERTICAL : DISPLAY_MODE_HORIZONTAL);
        }
    }
    
    // 3.Loại bỏ
    removeSushi();
}


Vậy là Xong, chạy và build thử xem thế nào nhé

OK, chạy ngon lành cành đào. 



Mình xin kết thúc bài này ở đây nhé. Tổng kết lại trong bài này ta học được 1 số thứ sau:
+ Đảo 2 Sushi bằng sự kiện Touch
+ Tạo ra các Sushi đặc biệt
+ Hiệu ứng khi ăn các Sushi đặc biệt

Hãy cài vào điện thoại và chơi thử nhé.

P/S: Vì đây chỉ là Demo cho 1 dạng game kiểu Candy Crush, nên chắc chắn sẽ còn nhiều thiếu sót lớn như:

+ Màn chơi
+ Gợi ý + Check hết nước đi
+ không ăn được nước kép dạng 2 chuỗi vuông góc
+ Khi tạo ra Sushi đặc biệt, 1 trong 2 Sushi swap sẽ trở lại vị trí cũ mà ko bị remove đi.
+ Khi ăn liên tiếp các chuỗi, có delay, không mượt cho lắm
+ Tính điểm, âm thanh, 
v..v...

Game Demo chỉ vậy thôi là ổn rồi. để phát triển nó cần phải có nhiều thời gian hơn. Trong khuôn khổ bài học, thì như vậy cũng chấp nhận được. Ai có tâm huyết game kiểu này có thể phát triển tiếp nhé. Bài này code cũng khá dài. Để hiểu kỹ, sâu, hoặc sửa lỗi, bổ sung, phát triển tiếp, các bạn nên đọc lại nhiều lần cho thấm nhé

Chào và hẹn gặp lại các bạn ở bài sau!

Download 

Code and Resource

Tuesday, June 3, 2014

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

Hi mọi người!

Chúng ta cùng tiếp tục bài học về game Sushi Crush nhé. Tổng kết lại bài trước 1 chút:
+ Tạo Class mới ( Sushi )
+ Tạo ma trận Sushi
+ Tạo Action rơi các sushi xuống ở màn chơi

Trong bài này chúng ta sẽ tập trung nghiên cứu 1 số vấn đề sau đây:

+ Kiểm tra và "ăn" 1 dãy ( 3-4-5 ) Sushi cùng loại
+ Tạo hiệu ứng "nổ" khi ăn Sushi
+ Lấp đầy khoảng trống do các Sushi đã bị ăn để lại trên ma trận

Tất cả các vấn đề trên đây đều xảy ra trong lớp PlayLayer nhé các bạn, lớp SushiSprite tạm thời sẽ không phải động tới Code.

(Bài này khá dài do code cũng dài, nên chỗ nào sai chính tả thì bỏ qua vậy)

Bắt đầu thôi!

Mở file PlayLayer.h. thêm code như sau

Phần Public:

    virtual void update(float dt) override; // Hàm này update game Scene theo thời gian dt ( 1/60 ở file AppDelegate.cpp đó)

Phần Private:

    bool m_isAnimationing; // biến kiểm tra việc đang ăn, rơi, hay hành động khác của Sushi hay không
    void checkAndRemoveChain(); // Kiểm tra và ăn dãy Sushi
    void getColChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList); // Kiểm tra tồn tại dãy Sushi theo cột hay không? - Lấy ra 1 List Sushi giống nhau ( kiểu &chainList là kiểu tham chiếu trong C+, dùng để thay đổi tham số truyền vào hàm thông qua việc lấy địa chỉ. Tuy giống con trỏ, nhưng nó có điểm khác con trỏ là ko phải dùng dấu *tên biến  để thao tác mà dùng trực tiếp tên biến )
    void getRowChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList); // Kiểm tra tồn tại dãy Sushi theo hàng hay không, Lấy ra bởi List
    void removeSushi(std::list<SushiSprite *> &sushiList);  // Xóa bỏ List Sushi, Ăn chuỗi Sushi
    void explodeSushi(SushiSprite *sushi); // Hiệu ứng nổ khi ăn Sushi
    void fillVacancies(); // Điền đầy khoảng trống do dãy Sushi đã bị ăn mất

B1 - Kiểm tra và ăn dãy Sushi cùng loại theo hàng và cột

Trước tiên, Mở file PlayLayer.cpp, thay đổi một số đoạn Code sau

// Kích thước ma trận 7x9
#define MATRIX_WIDTH (7)
#define MATRIX_HEIGHT (9)

PlayLayer::PlayLayer()
: spriteSheet(NULL)
, m_matrix(NULL)
, m_width(0)
, m_height(0)
, m_matrixLeftBottomX(0)
, m_matrixLeftBottomY(0)
, m_isAnimationing(true)  // Thêm vào hàm tạo
{
}

Trong hàm init(), thêm lệnh 

    scheduleUpdate(); // Update Scene theo thời gian

ở cuối trước return true;

Có lệnh scheduleUpdate(); thì phải có Hàm này để update theo dt


void PlayLayer::update(float dt)
    // Kiểm tra giá trị lần đầu của m_isAnimationing, mỗi bước thời gian dt, sẽ lại kiểm tra m_isAnimationing là true hay flase
    if (m_isAnimationing) { // nếu True
        // Gán = false
        m_isAnimationing = false;
        
        // Duyệt trong toàn ma trận 
        for (int i = 0; i < m_height * m_width; i++) {
            SushiSprite *sushi = m_matrix[i];

            // Nếu tồn tại 1 Sushi mà đang có "Action" thì  m_isAnimationing = true, và thoát vòng lặp
            if (sushi && sushi->getNumberOfRunningActions() > 0) {
                m_isAnimationing = true;
                break;
            }
        }
    }

    // Đến khi không có Action nào của Sushi tại bước thời gian dt nào đó, thì kiểm tra việc "Ăn" dãy Sushi nếu tồn tại

    if (!m_isAnimationing) { 
        checkAndRemoveChain();
    }
}

Kiểm tra việc ăn dãy Sushi giống nhau

void PlayLayer::checkAndRemoveChain()
{

    // Duyệt ma trận
    for (int i = 0; i < m_height * m_width; i++) {
        SushiSprite *sushi = m_matrix[i];

        if (!sushi) { // Rỗng thì bỏ qua
            continue;
        }
        
        // Đếm số lượng Sushi tạo thành chuỗi
        
        // Tạo 1 List để chứa các Sushi giống nhau
        std::list<SushiSprite *> colChainList; // Chuỗi Sushi theo cột
        getColChain(sushi, colChainList); // Lấy ra Chuỗi Sushi giống nhau theo cột, chú ý chỗ này ko còn dấu & giống như ở phần khai báo trong file .h, đây là cách dùng biến tham chiếu trong C++
        
        std::list<SushiSprite *> rowChainList; // Chuỗi Sushi theo hàng
        getRowChain(sushi, rowChainList); // Lấy ra Chuỗi Sushi giống nhau theo hàng

        // &longerList = biến tham chiếu
        // So sánh chuỗi dọc và chuỗi ngang, gán chuỗi lớn hơn cho longerList, tại sao lại có chỗ này, vì chỗ này vẫn trong vòng lặp với thứ tự Sushi thứ i có thể sẽ tồn tại 1 dấu CỘNG tạo bởi 2 chuỗi dọc và ngang cùng chứa Sushi i . Chơi candy, bejewer là biết
        std::list<SushiSprite *> &longerList = colChainList.size() > rowChainList.size() ? colChainList : rowChainList;

        // Chuỗi longer có 3 Sushi thì xóa bỏ chuỗi đó
        if (longerList.size() == 3) {
            removeSushi(longerList); // Ăn thôi
            return;
        }
        if (longerList.size() > 3) {
            // Tạo 1 Sushi Đặc biệt ở đây
            removeSushi(longerList);
            return;
        }
    }
}


Ta cùng tìm và kiểm tra sự tồn tại của Chuỗi các Sushi giống nhau theo hàng và cột trong ma trận qua 2 hàm sau đây

void PlayLayer::getColChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList)
{
    chainList.push_back(sushi); // Thêm vào dãy Sushi đầu tiên, tại vị trí thứ i đang xét trong vòng lặp FOR của hàm checkAndRemoveChain
 
    int neighborCol = sushi->getCol() - 1; // Xét cột bên trái
    while (neighborCol >= 0) { // Tồn tại cột bên trái

        // Tạo 1 pointer Sushi "bên trái" trỏ vào Sushi tại vị trí  (Hàng * width + neighborCol ), đây là cách quy ma trận cấp 2  về mảng 1 chiều nhé
        SushiSprite *neighborSushi = m_matrix[sushi->getRow() * m_width + neighborCol];

        // Nếu tồn tại sushi bên trái và cùng imgIndex (cùng loại Sushi) với sushi đang xét thì..
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            // Thêm sushi trái này vào list
            chainList.push_back(neighborSushi);
            neighborCol--; // Xét tiếp Sushi bên trái đến khi ko còn Sushi nào, cột 0
        } else {
            break;  // Ko thỏa mãn đk if ở trên, Phá vòng while
        }
    }
 
    neighborCol = sushi->getCol() + 1; // Xét Sushi bên phải
    while (neighborCol < m_width) { // Xét đến cột cuối cùng, cột cuối = m_width - nhé
        // Tương tự trên tìm ông sushi cùng loại bên trái
        SushiSprite *neighborSushi = m_matrix[sushi->getRow() * m_width + neighborCol];
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            chainList.push_back(neighborSushi); // Nhét vào List
            neighborCol++;
        } else {
            break; // Phá vòng while
        }
    }
}


// Giải thích Tương tự getColChain nhỉ
void PlayLayer::getRowChain(SushiSprite *sushi, std::list<SushiSprite *> &chainList)
{
    chainList.push_back(sushi);
 
    int neighborRow = sushi->getRow() - 1; // Xét sushi bên dưới
    while (neighborRow >= 0) {
        SushiSprite *neighborSushi = m_matrix[neighborRow * m_width + sushi->getCol()];
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            chainList.push_back(neighborSushi);
            neighborRow--;
        } else {
            break;
        }
    }
 
    neighborRow = sushi->getRow() + 1; // Xét sushi bên trên
    while (neighborRow < m_height) {
        SushiSprite *neighborSushi = m_matrix[neighborRow * m_width + sushi->getCol()];
        if (neighborSushi && (neighborSushi->getImgIndex() == sushi->getImgIndex())) {
            chainList.push_back(neighborSushi);
            neighborRow++;
        } else {
            break;
        }
    }
}


Và đây là hàm ĂN Sushi ( đọc lại hàm checkAndRemoveChain() để hiểu rõ cơ chế)

void PlayLayer::removeSushi(std::list<SushiSprite *> &sushiList)
{

    m_isAnimationing = true;
 
    std::list<SushiSprite *>::iterator itList; // Con trỏ duyệt trong List

    // Cú pháp vòng For duyệt 1 List trong C++ nâng cao
    for (itList = sushiList.begin(); itList != sushiList.end(); itList++) {

        // Chỗ này có vẻ hơi khó hiểu do nhiều dấu * nhỉ, Thế này nhé SushiSprite *sushi là tạo ra 1 con trỏ kiểu SushiSprite, (SushiSprite *) là ép kiểu con trỏ, *itList là giá trị chứa trong con trỏ, vì giá trị này lại là 1 con trỏ nên mới có việc ép kiểu con trỏ (SushiSprite *). iList là 1 con trỏ lại duyệt 1 mảng con trỏ nên có vẻ phức tạp thế này. Bạn hãy đọc lại về mảng con trỏ trong C++ là có thể hiểu

        SushiSprite *sushi = (SushiSprite *)*itList;
        // Loại bỏ sushi i ra khỏi ma trận
        m_matrix[sushi->getRow() * m_width + sushi->getCol()] = NULL;
        explodeSushi(sushi); // Tạo hiệu ứng nổ
    }
 
    // Rơi xuống để lấp đầy chỗ trống tạo bởi Sushi đã bị ăn
    fillVacancies();
}

Các hạm này sử dụng để kiểm tra và ăn các chuỗi Sushi giống nhau ( >3 ). Ta hãy cùng nghiên cứu 2 phần nhỏ sau

B2 - Tạo hiệu ứng nổ khi ăn Sushi

Vẫn trong PlayLayer.cpp, Bạn thêm 1 hàm này

void PlayLayer::explodeSushi(SushiSprite *sushi)
{

    // Thời gian hiệu ứng 0,3 giây
    float time = 0.3;

    // Thực hiện 2 hành động tuần tự, Co Sushi về kích thước, 0, 0, sau đó tự remove khỏi Contener cha
    sushi->runAction(Sequence::create(
                                      ScaleTo::create(time, 0.0), // Co kích thước về 0 trong thời gian 0.3
                                      CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, sushi)),
                                      NULL));
 
     // Action của Sprite tròn, mô phỏng vụ nổ

     auto circleSprite = Sprite::create("circle.png"); // Tạo mới sprite tròn
     addChild(circleSprite, 10);
     circleSprite->setPosition(sushi->getPosition()); // Vị trí = vị trí Sushi
     circleSprite->setScale(0); // Kích thước đầu =0
     // Thực hiện hành động tuần tự sau, Tăng kích thước lên tỷ lệ 1.0 trong thời gian 0,3 giây, sau đó xóa khỏi Layer
     circleSprite->runAction(Sequence::create(ScaleTo::create(time, 1.0),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, circleSprite)),
                                             NULL));

     // 3. Tạo hiệu ứng particleStars, CHÚ Ý

     auto particleStars = ParticleSystemQuad::create("stars.plist"); // Tạo mới
     particleStars->setAutoRemoveOnFinish(true); // Tự động remove khi xong việc
     particleStars->setBlendAdditive(false); // Thiết lập sự pha trộn thêm vào = false

     particleStars->setPosition(sushi->getPosition()); // Đặt vị trí tại Sushi nổ
     particleStars->setScale(0.3);  //  Thiết lập tỉ lệ 0.3
     addChild(particleStars, 20); // Thêm vào Layer Play 
}

Vâng hiệu ứng nổ và 1 chút màu mè đẹp mắt chỉ có vậy thôi. Tiếp theo là phần cho các Sushi rơi xuống điền đầy vào chỗ trống của các Sushi đã bị ăn

B3 - Lấp đầy khoảng trống do Sushi bị ăn để lại trên Ma trận

void PlayLayer::fillVacancies()
{
    Size size = CCDirector::getInstance()->getWinSize();
    // Chỗ này nhìn có vẻ phức tạp nhưng chẳng có gì đâu, chỉ là khai báo con trỏ, cấp phát bộ nhớ cho nó thôi, dùng như mảng 1 chiều
    int *colEmptyInfo = (int *)malloc(sizeof(int) * m_width);
    memset((void *)colEmptyInfo, 0, sizeof(int) * m_width); // set giá trị là 0 hết
 
    // Rơi Sushi đang có xuống khoảng trống
    SushiSprite *sushi = NULL; // Tạo 1 con trỏ Sushi = Null, 

    // Duyệt ma trận. Lưu ý ở đây 1 chút, chúng ta thường duyệt mảng 2 chiều theo thứ tự hàng, rồi đến cột, nhưng ở đây, hơi ngược 1 tý là cột rồi đến hàng. Và lưu ý rằng Cột 0, và Hàng 0 nằm ở vị trí bên Dưới phía Trái nhé. khi tạo ma trận ta cho viên Sushi 0,0 rơi xuống trước tiên mà

    for (int col = 0; col < m_width; col++) { // Duyệt theo cột, từ trái sang phải
        int removedSushiOfCol = 0;

        // Duyệt theo hàng, từ dưới lên trên
        for (int row = 0; row < m_height; row++) {
            sushi = m_matrix[row * m_width + col]; // Sushi tại vị trí hàng, cột
            if (NULL == sushi) { // Nếu rỗng
                removedSushiOfCol++; // Đếm số Sushi đã bị "ăn"
            } else { // Nếu ko rỗng
                if (removedSushiOfCol > 0) { // Nếu bên dưới nó có ô trống = số Sushi bị ăn
                    // Làm rơi xuống
                    int newRow = row - removedSushiOfCol; //Vị trí hàng mới ( giảm xuống )
                    // Trong ma trận ta bỏ sushi ở hàng row, và chuyển nó xuống dưới qua removedSushiOfCol ô rỗng
                    m_matrix[newRow * m_width + col] = sushi;
                    m_matrix[row * m_width + col] = NULL;
                    //Di chuyển
                    Point startPosition = sushi->getPosition();
                    Point endPosition = positionOfItem(newRow, col);
                    float speed = (startPosition.y - endPosition.y) / size.height; // Tốc độ
                    sushi->stopAllActions(); // Dừng mọi chuyển động trước đó của Sushi
                    sushi->runAction(MoveTo::create(speed, endPosition)); // Di chuyển = rơi xuống
                    // set hàng mới cho Sushi tại vị trí mới này
                    sushi->setRow(newRow);
                }
            }
        }
     
        // Mảng lưu trữ số lượng Sushi bị ăn tại vị trí Cột xác định
        colEmptyInfo[col] = removedSushiOfCol;
    }
 
    // 2. Tạo mới và làm rơi các Sushi xuống khoảng trống , lấp đầy ma trận
    for (int col = 0; col < m_width; col++) { // Duyệt cột từ trái sang phải

        // Duyệt hàng, chỉ xét từ vị trí rỗng trở lên
        for (int row = m_height - colEmptyInfo[col]; row < m_height; row++) {
            createAndDropSushi(row, col); // Tạo Sushi và rơi xuống vị trí Row, Col
        }
    }
 
    free(colEmptyInfo); // Giải phóng con trỏ 
}

Để dễ hình dung về việc rơi và điền đầy khoảng trống, hãy xem hình ảnh sau đây


Xong rồi, giờ bạn có thể Build và run code được rồi đó.

Tuy nhiên, không có điều gì xảy ra cả, kết quả ra vẫn gần giống bài trước thôi ( trừ trường hợp nào vào màn chơi mà đã ăn được Sushi tự nhiên )

Tổng kết lại trong bài này chúng ta học được 1 số điều thú vị sau đây:

+ Kiểm tra dãy Sushi cùng loại
+ Ăn khi thỏa mãn điều kiện dãy đó >=3 Sushi
+ Tạo hiệu ứng nổ khi ăn
+ Rơi các Sushi lấp đầy khoảng trống
+ Làm việc với List
+ Ma trận 2 chiều, 1 chiều, cách quy đổi 2 chiều sang 1 chiều
+ Làm quen với hệ thống trang trí particle trong game, sẽ tìm hiểu ở các bài sau

Download


Ở bài 3 chúng ta sẽ học cách di chuyển các Sushi, khi đó việc ăn Sushi, hiệu ứng nổ, rơi Sushi sẽ dễ dàng nhìn thấy hơn. Bài này chỉ là bài chuẩn bị cho bài sau thôi mà.

Chào và hẹn gặp lại ở bài sau