Member variable is corrupted in iOS, but works fine on Android

In my game, I have a custom class, Hero, that is derived from Node, and has a custom class, StateMachine to manage its state.

The StateMachine is derived from cocos2d::Ref, so that I can use Cocos’ automatic ref counting to manage its memory.

At the end of the Hero's init function, its StateMachine is correctly initialized and has a _refCount of 2, which reduces to 1 at the end of the first frame, which makes sense because the AutoReleasePool gets cleared then.

However, I’m finding that when the Hero's move function is invoked, its StateMachine variable points to a random new memory address, in which XCode debugger tells me has empty variables etc.

Here are some relevant code snippets:

// GameScene.hpp
class GameScene : public cocos2d::Layer {
private:
    // ...
    Hero* m_hero;
public:
    // ...
}

// GameScene.cpp
bool GameScene::init(const ValueMap& configuration) {
    // ... other config

    m_hero = Hero::create(refNode, heroConfig);
    if(!m_hero) {
        FATAL("Could not create a Hero!");
        return false;
    }
    m_hero->retain();

    // other stuff

    return true;
}

GameScene::onExit() {
    // ...
    CC_SAFE_RELEASE(m_hero);
    // ...
}
// Hero.hpp
class Hero : public Node {
private:
    // ...
    StateMachine* m_stateMachine;
public:
    static Hero* create(Node* heroNode, const ValueMap& configuration);
    // ...
}

// Hero.cpp
Hero* Hero::create(Node* heroNode, const ValueMap& configuration) {
    Hero* hero = static_cast<Hero*>(heroNode);

    if(hero && hero->init(configuration)) {
        hero->autorelease();

    } else {
        CC_SAFE_DELETE(hero);

    }

    return hero;
}

bool Hero::init(const ValueMap& configuration) {
    // ...

    m_stateMachine = StateMachine::create("HeroState");
    if(!m_stateMachine) {
        FATAL("Could not create a StateMachine!");
        return false;
    }
    m_stateMachine->retain();

    // ...

    // breakpoint here reveals:
    // m_stateMachine->_ID == 1042
    // m_stateMachine->_referenceCount == 2

    return true;
}

void Hero::moveToPosition(const Vec2& targetPosition) {

    // breakpoint here reveals:
    // m_stateMachine points to a different memory address
    // m_stateMachine->_ID == some random number
    // m_stateMachine->_referenceCount == some random number, but usually 256

    // never goes here because `isInState` returns false always, since this StateMachine
    // hasn't been initialized
    if(m_stateMachine->isInState(IDLE)) {...}
}

Also, using XCode debugger, during the Hero's init function, if I use expr m_stateMachine to get a reference to the StateMachine, and then later check this reference, it still exists, but its _ID and _referenceCount are also similarly changed, and the instance of Hero has lost the reference to it.

Here’s a screenshot of that flow, the this that is referenced is the instance of Hero:
debugger_screenshot

I have other modules that contain StateMachines but they don’t seem to have any of these issues.

Any idea what is going on here? Or do you have any suggestions to point me in the right direction?


Sidenote:

Also, I noticed that XCode thinks of the Hero object pointer as a Node* in the debugger variables view. Additionally, I’ve noticed that even though I have overriden Node's virtual void update(float delta) method, invoking m_hero->update(...) in GameScene calls Node's update. Which is wierd, because m_hero is a Hero* pointer. I got around that by creating an update function of a different name and using that, but I’m wondering if that’s related to this issue

debugger variables

@ricardo, can you look at this? Or @zhangxm

I created a GitHub issue: https://github.com/cocos2d/cocos2d-x/issues/16132

I managed to resolve this myself. The bug was in the way I was creating the Hero object.

Instead of static_cast'ing an existing Node into a Hero, I followed the pattern that CCSprite uses to subclass Node, by doing the following in Hero instead:

// Hero.cpp

// old fn header:
// Hero* Hero::create(Node* heroNode, const ValueMap& configuration) {
Hero* Hero::create(const ValueMap& configuration, const Vec2& startPosition) {
    // old code:
    // Hero* hero = static_cast<Hero*>(heroNode)
    Hero* hero = new Hero();

    if(hero && hero->init(configuration, startPosition)) {
    // ... rest is the same
}

// ctor is empty
Hero::Hero() {}

// initializer
bool Hero::init(const ValueMap& configuration, const Vec2& startPosition) {

    // Node is created here
    if(!Node::init()) {
        FATAL("Could not create the Hero's Node!");
        return false;
    }

    // the only reason I really needed that `heroNode` was to be able to
    // position a Node visually in Cocos Studio, and re-use the same
    // Node in the code to save a few ops
    //
    // Now I just do that manually here:
    setPosition(startPosition);

    // other config

    return true;
}

I still don’t fully understand what C++ was doing to cause it (my best guess right now is that it has something to do with ‘copy elision’ - link).

Something about static_cast doesn’t actually convert the pointer to the derived class. So that caused any extra member variables that don’t exist in the base class, Node, to fall out of scope and get leaked. Which would explain why they still exist in the AutoReleasePool (since they haven’t been explicitly released yet), but don’t exist as a reference in the fake Hero* pointer.

The XCode Leaks Instrument also tells me there are no leaks now, which is good.


If you have more of an idea as to why this solution works, do let me know!

Actually, you can’t take any Node and just cast it to another class :slight_smile: Or am i wrong and you are casting existing Hero node instance? But in that case code looks slightly weird and should work.

Anyway, your solution works because it implemented correctly :wink: You have created new Hero instance and initialized it.

I would recommend you to use dynamic_cast instead when you want to cast BaseClass to DerivedClass. It will return nullptr if object is not instance of DerivedClass.