I started developing a slots game using the Cocos2d C++ SDK. Cocos2d is an opensource game development framework that support C++, Objective-C, and JavaScript. Eventually, this game will evolve into a slots RPG, like King Cashing and King Cashing 2. For now it uses a pretty generic reels and a match 3 mechanic that matches on the same row as well as cells on adjacent rows. For score, it keeps experience points, since this will transition to the RPG slots.
Source for the project can be found here. As of this writing, it’s in early development.
Like many game frameworks, Cocos2d has many helper functions that allow for quick game prototyping. Scenes are easy to construct, and assets, sprites, and audio can be added using built in Cocos2d objects. It even supports the ability to add custom shaders.
Extending Sprites
I found quickly that I needed to create custom objects that extended sprites. In this project there two classes that extend cocos2d::sprite ; the reel, and the HUD.
Grouping elements within sprites helped with organizing code and separation of concern. I did run into strange memory errors when trying to add certain objects, such as cocos2d::Label , directly to the scene while also having a pointer to it in the scene.
Autorelease
The Cocos2d C++ framework uses a smart-pointer technique to automatically destroy dynamically allocated objects when its internal reference count is 0. This relieves pressure in remembering to destroy objects and worrying about pointer ownership. Though cyclical dependencies still need to be avoided.
The built-in Cocos2d objects are automatically added to the autorelease pool, so there is no need to use the new keyword. In my project, I have an object that extends the Cocos2d sprite. So there’s some boilerplate code that I needed to add so my object would be added to the autorelease pool.
ReelSprite* ReelSprite::create(const std::string& filename, std::vector<int> _cells) { ReelSprite* mainSprite = new ReelSprite(); mainSprite->init(filename); mainSprite->autorelease(); // <-- ADD TO AUTORELEASE POOL mainSprite->cells = _cells; return mainSprite; }
ReelSprite::create is a static method that follows the Cocos2d convention of constructing an object and adding it to the autorelease pool. mainSprite->autorelease() is the line that actually adds the object to the autorelease pool, so that it does not have to be manually destroyed.
Reel Animation
To get the reels to function properly in regards to animation and properly stopping, there were several things that had to happen. The reels needed to be animated so they moved down vertically and timing needed to be involved so that they stopped within a given time and at a given cell.
Spin Management
In this implementation the start of each reel is staggered using the scheduler. This is something that will probably change. The ReelSprite::startSpin method is called by the scheduler.
auto scheduler = Director::getInstance()->getScheduler(); scheduler->schedule([this, stop_positions](float dt) { this->_reel2->startSpin(this->_reel1->spinInterval, stop_positions[1]); }, this, 0.3f, false, 0.0f, false, "myCallbackKey1");
In ReelSprite::startSpin, there’s another scheduled callback function that handles the stop sequence. I’ve broken this process down below. It starts with defining the expected position, which is challenging because there’s two sprites that alternate top-bottom order.
scheduler->schedule([this, stop_position](float dt) { int adjusted_stop_position = stop_position + 2; if (adjusted_stop_position >= this->_numCells) { adjusted_stop_position -= this->_numCells; } Vec2 position1 = this->_reel1->getPosition(); Vec2 position2 = this->_reel2->getPosition(); cocos2d::log("Desired stop position %d", stop_position); // Calculate y cell position relative to bottom of a single strip instance float new_y = -(this->_cellHeight * stop_position) + (this->_cellHeight * 2); Vec2 position; if (position1.y < position2.y && (position1.y - new_y) >= 0 || position1.y > position2.y && (position2.y - new_y) <= 0) { // First strip is below second position = position1; cocos2d::log("POSITION 1"); } else { // First strip is above second position = position2; cocos2d::log("POSITION 2"); } if (position.y != new_y) { // Animate stop } else { // Stop immediately } }
If the desired cell is not at the right position, cocos2d actions are used to move the reels to the right position while maintaining the spin animation.
The tricky thing I ran into was I needed to animate the two sprites contained in the reel at the same time. This was accomplished using cocos2d::Sequence. The challenge here is two fold; binding the sprite in question, and handling a race condition.
_isStopping = true; CallFunc* callback = CallFunc::create(this, callfunc_selector(ReelSprite::stopSpin)); _stopSequence = Sequence::create(Spawn::create(TargetedAction::create(this->_reel1, moveBy1), TargetedAction::create(this->_reel2, moveBy2)), callback, NULL); this->runAction(_stopSequence);
It’s possible the callback is called twice. Luckily cocos2d::Sequence::isDone exists to detect if the sequence is indeed finished. From there, other flags are set to indicate the reel has completely stopped spinning.
void ReelSprite::stopSpin() { if (_stopSequence->isDone()) { cocos2d::log("Sprite after move %s", std::to_string(this->_reel1->getPosition().y).c_str()); this->_isSpinning = false; _isStopping = false; this->_audioMgr->playEffect("stop-reel.mp3", false, 1.0f, 1.0f, 1.0f); } }
Spinning Animation
In order to accomplish a fluid animation, each reel column needed two instances of the reel image to fill in a gap left when the top of one passed the top of the view window.
void ReelSprite::incrementSpin(float delta) { if (_isSpinning) { Vec2 position1 = this->_reel1->getPosition(); Vec2 position2 = this->_reel2->getPosition(); float height; if (!_isStopping) { position1.y -= this->speed * delta; position2.y -= this->speed * delta; } height = this->_reel1->getBoundingBox().size.height; int top_y = position1.y + height; if (top_y < 0) { // Place sheet above secondary position1.y = position2.y + height; } this->_reel1->setPosition(position1); height = this->_reel2->getBoundingBox().size.height; top_y = position2.y + height; if (top_y < 0) { // Place sheet above first position2.y = position1.y + height; } this->_reel2->setPosition(position2); } }
Moving Forward
Though my use of Cocos2d has been somewhat limited, so far I really like Cocos2d. It is well suited for 2d games, and I will use it more to see how well it compares to other options, such as Unity.
There is also some refactoring that will need to be done, for example with lambdas and matching.
Lambdas
I used lambda functions in my code because it was convenient for prototyping. Unfortunately, the use of lambdas make it more difficult to unit test.
void ReelSprite::startSpin(int interval_buffer, int stop_position) { ... scheduler->schedule([this, stop_position](float dt) { ... } }
For example, crucial logic that handles the stopping sequence is embedded in ReelSprite::startSpin . Ideally this should be broken out so that the stop sequence can be tested independently.
Matching
At the moment, matching is pretty flawed because it will match cells across the same column. I intend on covering this in another post that will involve the recursive function that does the matching, as well as unit tests that assert the proper matches are being returned.