Hướng Dẫn Code Game Bằng C++ (Phần 3)

Hướng Dẫn Code Game Bằng C++ (Phần 3)

Trong các phần trước, mình đã giới thiệu các bạn những điểm cơ bản nhất của Cocos2d-x và hướng dẫn code game Pikachu classic. Trong phần này, mình sẽ hướng dẫn các bạn bổ sung các tính năng cho game của mình. Hãy cùng bắt đầu nào.

1. Action

Trong cocos2d-x, Action là một loại Node được sử dụng để thực hiện hành động hoặc thay đổi tính chất của các Node khác.

Một số loại Action cơ bản như MoveTo, MoveBy, RotateTo, RotateBy, ScaleTo, ScaleBy, FadeIn, FadeOut, ...

Mỗi Action thường có 2 loại By và To. Các Action loại By thực hiện thay đổi phụ thuộc vào trạng thái hiện tại của Node, còn các Action loại To thì không.

Ví dụ:

auto moveBy = MoveBy::create(2, Vec2(500, 200));

mySprite->runAction(moveBy);

MoveBy di chuyển mySprite đến vị trí cách vị trí hiện tại 500 đơn vị trên tọa độ x và 200 đơn vị trên tọa độ y trong vòng 2 giây. Tức là nếu vị trí hiện tại của mySprite là (x, y) thì sau 2 giây, mySprite sẽ được dần dần di chuyển đến vị trí (x+500, y+200).

auto moveTo = MoveTo::create(2, Vec2(500, 200));

mySprite->runAction(moveTo);

MoveTo di chuyển mySprite đến đúng vị trí (500,200) trong vòng 2 giây. Tức là cho dù ban đầu mySprite ở đâu thì sau 2 giây, mySprite sẽ được di chuyển đến vị trí (500, 200).

Có một số loại Action đặc biệt:

Sequence::create(action1, action2,…);

- Thực thi các Action theo thứ tự.

Spawn::create(action1, action2,…);

- Thực thi đồng thời nhiều Action. Giống như dùng nhiều lệnh runAction nhưng có thể được cho vào Sequence.

CallFunc::create(function);

- Thực thi hàm function.

DelayTime::create(t);

- Delay t giây. Thường được dùng để giãn cách giữa 2 Action trong một Sequence.

Repeat::create(action, n);

- Lặp đi lặp lại action, n lần.

RepeatForever::create(action);

- Lặp đi lặp lại một hành động không ngừng (trừ khi gọi hàm stopAction).

TargetedAction::create(target, action);

- Gắn mục tiêu của hành động là target. Nghĩa là target sẽ là Node thực thi action, thay vì Node gọi runAction.

Trong bài viết này, mình sẽ sử dụng Action để điều khiển thanh thời gian và các hiệu ứng hình ảnh của trò chơi.

2. Progress Timer

Nếu bạn muốn biểu diễn thời gian chạy, tiến độ (bao nhiêu phần trăm trước khi qua bàn), thanh đang tải (Loading)… trong cocos2d-x, cách dễ dàng nhất là sử dụng ProgressTimer. Đây là một Node có dạng ảnh và có thể thu nhỏ, phóng to theo tỷ lệ từ 0% đến 100%.

bool GameScene::init()
{

	...

	showProgressTimer();

	return true;
}

Hàm showProgressTimer() sẽ thực hiện tạo và xử lý ProgressTimer.

ProgressTimer có 2 loại:

2.1. RADIAL: Dạng vòng tròn

Cài đặt hàm GameScene::showProgressTimer() - RADIAL version:

void GameScene::showProgressTimer()
{
	auto screenSize = Director::getInstance()->getVisibleSize();
	auto board = boardView->getBoundingBox();

	auto progressTimer = ProgressTimer::create(Sprite::create("ProgressCircle.png"));
	progressTimer->setType(ProgressTimer::Type::RADIAL);
	progressTimer->setMidpoint(Vec2(0.5f, 0.5f));
	progressTimer->setReverseDirection(true);
	progressTimer->setPercentage(100);
	progressTimer->setScale((screenSize.height - board.getMaxY()) / 1.5f / progressTimer->getContentSize().width);
	progressTimer->setPosition(screenSize.width * 3 / 5, (screenSize.height + board.getMaxY()) / 2);
	this->addChild(progressTimer);
	progressTimer->runAction(ProgressFromTo::create(60, 100, 0));
}

Đây là bộ đếm ngược có dạng hình tròn.

  • ProgressTimer::create(Sprite::create("ProgressCircle.png"));
    • Tạo ProgressTimer từ ảnh
  • progressTimer->setType(ProgressTimer::Type::RADIAL);
    • Chọn loại RADIAL
  • progressTimer->setMidpoint(Vec2(0.5f, 0.5f));
    • Đặt tâm của vòng tròn
  • progressTimer->setReverseDirection(true);
    • Xoay theo chiều kim đồng hồ. (ngược chiều thì setReverseDirection(false))
  • progressTimer->setPercentage(100);
    • Đặt ban đầu 100%
  • progressTimer->setScale((screenSize.height - board.getMaxY()) / 1.5f / progressTimer->getContentSize().width);
    progressTimer->setPosition(screenSize.width * 3 / 5, (screenSize.height + board.getMaxY()) / 2);
    this->addChild(progressTimer);
    • Cài đặt kích cỡ, vị trí và thêm progressTimer vào GameScene
  • progressTimer->runAction(ProgressFromTo::create(60, 100, 0));
    • Chạy đếm ngược từ 100% về 0% trong vòng 60 giây.

2.2. BAR: Dạng thanh dọc hoặc ngang

Cài đặt hàm GameScene::showProgressTimer() - BAR version:

void GameScene::showProgressTimer()
{
	auto screenSize = Director::getInstance()->getVisibleSize();
	auto board = boardView->getBoundingBox();

	auto progressTimer = ProgressTimer::create(Sprite::create("ProgressBar.png"));
	progressTimer->setType(ProgressTimer::Type::BAR);
	progressTimer->setMidpoint(Vec2(0.0f, 0.5f));
	progressTimer->setBarChangeRate(Vec2(1.0f, 0.0f));
	progressTimer->setPercentage(100);
	progressTimer->setScale(screenSize.width / progressTimer->getContentSize().width);
	progressTimer->setPosition(screenSize.width / 2, boardView->getBoundingBox().getMinY() / 2);
	this->addChild(progressTimer);
	progressTimer->runAction(ProgressFromTo::create(60, 100, 0));
}

Đây là bộ đếm ngược dạng thanh ngang:

  • progressTimer->setMidpoint(Vec2(0.0f, 0.5f)); // Vector 2d 
    • Đặt tâm ở điểm giữa trái. Ảnh của progressTimer sẽ thu về phía Midpoint.
  • progressTimer->setBarChangeRate(Vec2(1.0f, 0.0f));
    • Đặt tỷ lệ thay đổi của 2 chiều ngang, dọc.
  • Phần còn lại tương tự RADIAL

3. Hiệu ứng hình ảnh

Một phần quan trọng không thể thiếu đó chính là các hiệu ứng hình ảnh. Hiệu ứng khi chọn pokemon, khi nối pokemon, khi pokemon biết mất, ...

Hàm BoardView::onTouchPokemon:

bool BoardView::onTouchPokemon(Touch* touch, Event* event) {
	auto touchLocation = touch->getLocation() - this->getPosition();
	auto target = event->getCurrentTarget();
	if (target->getBoundingBox().containsPoint(touchLocation)) {
		auto p = findRowAndColumnOfSprite(target);
		removeChoosePokemonEffect();
		if (board->selectPokemon(p.first, p.second)) {
			connectPokemons(board->_x, board->_y, p.first, p.second);
			board->_x = board->_y = -1;
		}
		else {
			createChoosePokemonEffect(pokemons[p.first][p.second]);
			board->_x = p.first;
			board->_y = p.second;
		}
		return true;
	}
	return false;
}

Mỗi khi người dùng chạm vào một pokemon sẽ có 2 trường hợp có thể xảy ra:

  • Một là nối được 2 pokemon. Lúc này mình sẽ làm những hiệu ứng sau:
    • Tạo đường nối giữa 2 pokemon
    • Làm mờ dần 2 pokemon
    • Xóa 2 pokemon
  • Hai là không nối được, tức là chọn một pokemon. Lúc này mình sẽ tạo hiệu ứng để làm nổi bật pokemon được chọn.

3.1. Hàm BoardView::connectPokemons:

Hàm tạo hiệu ứng khi nối được 2 pokemon.

void BoardView::connectPokemons(int x, int y, int _x, int _y) {
	// 1: Hieu ung noi 2 pokemon
	auto connectEffect = getConnectEffect(x, y, _x, _y);
	
	// 2: Hieu ung lam mo 2 pokemon
	auto pokemonFade1 = TargetedAction::create(pokemons[x][y], FadeOut::create(0.5));
	auto pokemonFade2 = TargetedAction::create(pokemons[_x][_y], FadeOut::create(0.5));
	auto effectSpawn = Spawn::create(pokemonFade1, pokemonFade2, nullptr);
	
	// 3: Xoa 2 pokemon
	auto removePokemon1 = CallFunc::create(CC_CALLBACK_0(BoardView::removePokemon, this, x, y));
	auto removePokemon2 = CallFunc::create(CC_CALLBACK_0(BoardView::removePokemon, this, _x, _y));
	auto removePokemonSpawn = Spawn::create(removePokemon1, removePokemon2, nullptr);
	
	// Sequence(1, 2, 3)
	auto sequence = Sequence::create(connectEffect, effectSpawn, removePokemonSpawn, nullptr);
	this->runAction(sequence);
}
  • auto connectEffect = getConnectEffect(x, y, _x, _y);
    • connectEffect là hiệu ứng đường nối giữa 2 pokemon.
    • Hàm getConnectEffect tìm đường nối giữa 2 pokemon, tạo một ParticleSystem ở đầu đường nối và vẽ đường nối bằng cách di chuyển ParticleSystem này theo một Sequence.
FiniteTimeAction* BoardView::getConnectEffect(int x, int y, int _x, int _y) {
	auto path = board->findPath(x, y, _x, _y);
	std::vector<Vec2> _path;
	for (auto p : path) {
		_path.push_back(positionOf(p.first, p.second));
	}
	
	auto emitter = ParticleSystemQuad::create("path.plist");
	this->addChild(emitter);
	float duration = 0.3f;
	emitter->setDuration(duration);
	emitter->setPosition(_path[0]);
	Vector<FiniteTimeAction*> actions;
	for (int i = 1; i < _path.size(); ++i) {
		actions.pushBack((FiniteTimeAction*)MoveTo::create(duration / (_path.size() - 1), _path[i]));
	}
	return TargetedAction::create(emitter, Sequence::create(actions));
}
  • auto pokemonFade1 = TargetedAction::create(pokemons[x][y], FadeOut::create(0.5));
    auto pokemonFade2 = TargetedAction::create(pokemons[_x][_y], FadeOut::create(0.5));
    auto effectSpawn = Spawn::create(pokemonFade1, pokemonFade2, nullptr);
    • Sử dụng Spawn để đồng thời tạo hiệu ứng FadeOut làm mờ 2 Pokemon trong 0.5 giây.
  • auto removePokemon1 = CallFunc::create(CC_CALLBACK_0(BoardView::removePokemon, this, x, y));
    auto removePokemon2 = CallFunc::create(CC_CALLBACK_0(BoardView::removePokemon, this, _x, _y));
    auto removePokemonSpawn = Spawn::create(removePokemon1, removePokemon2, nullptr);
    • Sử dụng Spawn để xóa 2 pokemon.
  • auto sequence = Sequence::create(connectEffect, effectSpawn, removePokemonSpawn, nullptr);
    this->runAction(sequence);
    • Sử dụng Sequence để thực hiện 3 hành động trên liên tiếp nhau.

3.2. Tạo và xóa hiệu ứng chọn pokemon

Tạo pokemon: Hàm BoardView::createChoosePokemonEffect :

void BoardView::createChoosePokemonEffect(Node* pokemon)
{
	auto emitter = ParticleSystemQuad::create("fireworks.plist");
	auto square = pokemon->getBoundingBox();
	emitter->setPosition(square.getMinX(), square.getMinY()); //Dat hieu ung ban dau o goc trai duoi pokemón

	// Tao hieu ung particle chay quanh pokemon
	auto moveUp = MoveBy::create(0.2, Vec2(0, squareSize));
	auto moveRight = MoveBy::create(0.2, Vec2(squareSize, 0));
	auto moveDown = MoveBy::create(0.2, Vec2(0, -squareSize));
	auto moveLeft = MoveBy::create(0.2, Vec2(-squareSize, 0));
	auto sequence = RepeatForever::create(Sequence::create(moveUp, moveRight, moveDown, moveLeft, nullptr));
	emitter->runAction(sequence);

	// Chay hieu ung
	this->addChild(emitter, 2);
	emitter->setName("choosePokemon");
}
  • Hàm này tạo một ParticleSystem chạy quanh pokemon được chọn. Sử dụng RepeatForever để hiệu ứng chạy liên tục.

Khi chọn một pokemon khác hoặc nối một cặp pokemon, phải xóa hiệu ứng chọn ở pokemon trước. Vậy nên khi thêm hiệu ứng vào BoardView mình đã sử dụng hàm setName để đặt tên node emitter"choosePokemon". Sau khi đặt tên cho node, mình có thể truy cập node từ node cha một cách dễ dàng bằng hàm getChildByName.

Hàm xóa hiệu ứng chọn pokemon: 

void BoardView::removeChoosePokemonEffect() {
	if (this->getChildByName("choosePokemon") != nullptr)
		this->removeChildByName("choosePokemon");
}

4. Audio

Các hiệu ứng âm thanh cũng là một phần không kém quan trọng khi lập trình game. Game mà không có tiếng động thì thật là nhàm chán phải không?

Trong cocos2d-x, AudioEngine là một class chỉ gồm các hàm static (hàm chung của class) để quản lý âm thanh, tiếng động trong game. AudioEngine rất dễ sử dụng, nó có các hàm như:

  • int play2d(const string& musicFileName, bool loop, float volume);
    • Bật nhạc từ file có tên là musicFileName, loop là true nếu nhạc được bật đi bật lại (ví dụ như nhạc nền - background music) và là false nếu nhạc chỉ bật một lần (ví dụ như các hiệu ứng âm thanh - sound effects), volume là âm lượng nhạc.
    • Hàm này trả về một số nguyên là id của âm thanh. Bạn có thể dùng id này để điều khiển các thuộc tính từng âm thanh.
  • setVolumn(id, float), getVolumn(id)
    • Đặt âm lượng, lấy âm lượng.
  • stop(id), stopAll(), pause(id), pauseAll(), resume(id), resumeAll(), setCurrentTime(id, time), getCurrentTime(id)
    • Là các hàm điều khiển âm thanh: dừng âm thanh id, dừng toàn bộ âm thanh, ...

Mình sẽ tạo một class AudioManager để điều khiển âm thanh của game. Class sẽ quản lý 3 loại âm thanh: nhạc nền, hiệu ứng âm thanh khi chọn pokemon, hiệu ứng âm thanh khi nối 2 pokemon.

AudioManager.h:

#pragma once

class AudioManager
{
public:
	static int backgroundMusic;

	static float backgroundVolume, effectVolume;

	static void playBackgroundMusic();

	static void stopBackgroundMusic();

	static void setBackgroundVolume(float volume);

	static void playSelectPokemonSoundEffect();

	static void playRemovePokemonSoundEffect();

	static void setEffectVolume(float volume);

};

AudioManager.cpp:

#include "AudioManager.h"
#include "AudioEngine.h"

int AudioManager::backgroundMusic = -1;

float AudioManager::backgroundVolume = 1.0f;

float AudioManager::effectVolume = 1.0f;

void AudioManager::playBackgroundMusic()
{
	backgroundMusic = cocos2d::AudioEngine::play2d("backgroundMusic.mp3", true, backgroundVolume);
}

void AudioManager::stopBackgroundMusic()
{
	cocos2d::AudioEngine::stop(backgroundMusic);
}

void AudioManager::setBackgroundVolume(float volume)
{
	backgroundVolume = volume;
	cocos2d::AudioEngine::setVolume(backgroundMusic, volume);
}

void AudioManager::playSelectPokemonSoundEffect()
{
	cocos2d::AudioEngine::play2d("selectPokemonSoundEffect.mp3", false, effectVolume);
}

void AudioManager::playRemovePokemonSoundEffect()
{
	cocos2d::AudioEngine::play2d("removePokemonSoundEffect.mp3", false, effectVolume);
}

void AudioManager::setEffectVolume(float volume)
{
	effectVolume = volume;
}

Khởi động nhạc nền ngay khi khởi tạo scene HelloWorld:

Scene* HelloWorld::createScene()
{
    auto helloWorld = HelloWorld::create();
    AudioManager::playBackgroundMusic();
    return helloWorld;
}

Tạo hiệu ứng âm thanh khi chọn pokemon:

void BoardView::createChoosePokemonEffect(Node* pokemon)
{

	...

	AudioManager::playSelectPokemonSoundEffect();
}

Tạo hiệu ứng âm thanh khi nối 2 pokemon:

void BoardView::createRemovePokemonEffect(Node* pokemon) 
{

	...

	AudioManager::playRemovePokemonSoundEffect();
}

4. Tổng kết

Như vậy là qua 3 bài viết, mình đã giới thiệu cho các bạn hầu hết toàn bộ các khái niệm yếu tố cơ bản nhất của cocos2d-x, một game engine bằng C++.

Dĩ nhiên là game hiện tại vẫn còn thiếu sót, nhưng từ những gì mình đã giới thiệu (Scene, Node, Sprite, Layer, Event, Action, Audio) các bạn hoàn toàn có thể sáng tạo, hoàn thiện game theo ý thích của mình.  

Mong các bạn cảm thấy bài viết của mình có ích :D

Tham khảo:

Phần 1: https://codelearn.io/blog/view/huong-dan-lam-game-bang-cocos2d-x-phan-1

Phần 2: https://codelearn.io/blog/view/huong-dan-lam-game-bang-cocos2d-x-phan-2

Code của mình: https://github.com/s34vv1nd/Cocos2dx-Tutorials