cocos2d / cocos2d-js__old__

cocos2d in JavaScript central repository
14 stars 9 forks source link

Change some parts to Component Based Design #64

Open walzer opened 11 years ago

walzer commented 11 years ago

We met some problems when changing from objective-c to c++.

Problem in OOP

A bad sample is CCPhysicsSprite.cpp, it's very ugly, in c++ we need to

  1. repeat the static creator functions of CCSprite, the declaration and implementation is totally the same.
  2. overwrite all setter & getters of transforms.
  3. using marcos to mix chipmunk & box2d in the same code. Well, it's only for physics feature in CCSprite. If we want to add physics to CCLabel, CCMenuItem, CCProgressBar, that will be a nightmare.

Another bad sample is CCNode.h. It's too fat that includes almost everything in it, including:

As a result, if I want to put an audio source in a position, the volume will be increase when the hero close to it and decrease when hero leaves it. What can I do? I have 2 choices

  1. Write game logics in layer, where calculates the distance between audio source and hero, and change the volume. It works for simple games but if you have 100 objects in current scene, writing the logics about how objects affect others in layer::update() is undoable.
  2. A better approach is to inherit CCNode to e.g. MyAudioSource, for only use its position and scheduler. In MyAudioSource::update(), I found the position of hero via this->getParent()->getChild(TAG_HERO)->getPosition(), calculate it then change the volume. A programmer can keep his code neat, without mix his logic with other logics in MyLayer::update(). But in this way, MyAudioSource inherits too much unneeded features like user data, actions, script handlers.

    Think in Components

I have an idea that we should improve a bit from object-orientated to component-based. For example, reimplement CCPhyscisSprite to something like PhysicsComponentChipmunk & PhysicsComponentBox2d. We can have

To do this, I suggest that we can add a "component container" in CCNode or wrap CCNode to RendererComponent then add it to a high level GameObject/Entity. Both of these approaches won't break the compatibility to earlier coco2d versions. Then we should add the communication mechanism that allows components in a same CCNode to communicate.

Other Benefits

  1. Easier Data Driven So far in CocosBuilder, our data can only drive graphics, but can't drive physics, audios, script callbacks, messages, etc. That's insufficient to compose a whole game. If you want to add more features into a CCNode object, we need to implement something like CCPhysicsSprite in object-orientated way, or add components into CCNode.
  2. Editor Friendly CocosBuilder shows the inheritance in its right sidebar. But editors can not create inherited classes to add new features. If I add physics feature to CCSprite, I need to code CCPhysicsSprite firstly, then add a plugin to CocosBuilder, and finally code it in CCBReader. In component design, we only need the prepared physics component.

    References

ricardoquesada commented 11 years ago

Very useful info.

Does this issue deprecate issue #10 ?

walzer commented 11 years ago

@ricardoquesada I think the relationship between this issue and #10 is that, issue #10 is talking about the "ControllerComponent" in this component-based structure. So #10 can be a part of this issue. In fact, with component-based design, everything can be a part of this component-baed structure, as I said to you in Beijing.

ControllerComponent or EventsReceiverComponent

The "ControllerComponent" can looks like

// usage
CCSprite* hero = CCSprite::create("hero.png");
hero->addComponent( HeroController::create() );  // hero node is the owner of HeroController component.
hero->addComponent( PhysicsComponentChipmunk::create() ); // hero has physics feature now

// HeroControllerComponent implements the interfaces of ControllerComponent
void HeroController::onTouchBegan(CCTouch* touch)
{
    this->getOwner()->setPosition(x, y);  // ControllerComponent communicates with owner
    this->getOwner()->getComponent("physics")->setPosition(x,y);  // communicates with other component
}

void HeroController::onUpdate()
{
    // do something
}

// PhysicsComponent is prepared by engine
void PhysicsComponentChipmunk::setPosition(x, y)
{
    cpBodySetPos(m_pBody, ccv(x,y))
}

Let's call it "ControllerComponent" or "EventsComponent", while messaging is another issue. In C++, this line `this->getOwner()->getComponent("physics")->setPosition(x,y)" is a bad practice, owner and component may not exists. In defensive programming way, we should write it like this:

do
{
    CCNode* owner = this->getOwner();
    if (!owner) break;
    CCComponent* physics = dynamic_cast<CCComponentPhysicsChipmunk*>(owner->getComponent("physics");
    if (!physics) break;
    physcis->setPosition(x,y);
} while(0)

Well, that's the communication between components and its owner node.

MessageComponent

The source code above shows how component-based structure works. But it's not good enough, since it couples the owner + ControllerComponent + PhysicsComponent with function calls. The messaging is a replacement of direct function call, it should looks like:

// HeroControllerComponent implements the interfaces of ControllerComponent
void HeroController::init()
{
    _message = this->getOwner()->getComponent("message");
}

void HeroController::onTouchBegan(CCTouch* touch)
{
    // using messages instead of direct function calls in c++

    // setter
    _message->send<CCPoint>("node", "setPosition", ccp(x,y));
    _message->send<CCPoint>("physics", "setPosition", ccp(x,y));
    _message->send<bool>("node", "setVisible", true);

    // getter
    float health = _message->send<float>("attribute", "getHealth");
}

Objective-c / lua / javascript has reflection, they're very easy to convert strings to function calls. So MessageComponent is only required in C++ games, it's expensive on performance, so it's only an option. Developers can choose to use MessageComponent or just direct function calls. We don't need to bind MessageComponent to javascript. MessageComponent is a low priority issue.

totallyeviljake commented 11 years ago

walzer, this line:

CCComponent* physics = dynamic_cast<CCComponentPhysicsChipmunk*>(owner->getComponent("physics");

This is very expensive because it requires a vtable lookup on the virtual type to cast it to the proper object type. If you are doing this in the game update loop, which is being run millions of times in your game, the latency caused by this one virtual lookup is noticeable.

Component models for games are OK so long as it makes sense for the game. I don't recommend that you componentize the physics features from box2d or chipmunk. While it is not pretty, I recommend using idioms to include hard typed pointers to Chipmunk or box2d objects.

avoid any virtual methods at all costs.

nickveri commented 11 years ago

This is an interesting idea, do you think, though, that the component based architecture should be followed all the way through? What I mean is, should the end goal be an "empty" game object that you can add components to? So instead of adding a physics component to a sprite object you could have something like this:

GameObject *hero = GameObject::create();
hero->addComponent( SpriteComponent::create("hero.png") ); // renders a texture
hero->addComponent( TransformComponent::create() ); // handles scene-graph transformations
hero->addComponent( PhysicsComponentChipmunk::create() ); // physics object

This will also separate the rendering code to the "renderable" components.

totallyeviljake commented 11 years ago

This component based system is interesting from a researchy point of view, but a game designer will know ahead of time whether or not they want a physics game or a non-physics game. It would be better for the end user (the game developer) if the GameObject were specialized as a Box2DGameObject, ChipmunkGameObject, or GameObject (with no physics).

The moment you start to do loose coupling of game features is the same moment that cocos2d starts to suffer in performance. cocos2d's success has been its fantastic performance and easy interface (and incredible implementation of cocoa across the big 3).

We did all of that refactoring of the ::create() and init() and property accessors in cocos2d-xna and it was no easy task. We're still refactoring and optimizing.

On a secret note, we have a private project that is merging chipmunk and box2d so it will be transparent to the game designer. chipmunk is crazy fast!

walzer commented 11 years ago

@nickveri I've considered about this way. AFAIK, many game companies are using cocos2d like this.They wrapped a game object on the top of cocos2d, use cocos2d as the renderer. But I'm afraid that this way changes cocos2d developers’ habit too much, and totally break the forward compatibility.

walzer commented 11 years ago

@totallyeviljake I planned to use a ComponentContainer to optimise frequently used components, looks like this.

class ComponentContainer
{
protected:
    // for user defined components
    CCDictionary* m_pComponentMap;

    // Optimisations. Don't look up frequently used components from dictionary
    CCComponent* m_pController;
    CCComponent* m_pPhysics;
    CCComponent* m_pScript;
public:
    CCComponent* get(std::string key) const;
    {
        if ( key == "physics")
        {
            return m_pPhysics;
        }
        else if ( key == "controller") 
        {
            return m_pController;
        }
        else if ( key == "script")
        {
            return m_pScript;
        }
        else
        {
            return dynamic_cast<CCComponent*>(m_pComponentMap->objectForKey(key));
        }
    }
};

Physics component is just a sample, we also need to split the scripting members from CCNode to a component. Component is not convenient at coding, GameObject *hero = GameObject::create(); hero->addComponent( PhysicsComponentChipmunk::create() ); is not convenient as hero = PhysicsChipmunkGameObject::create() when coding. But it's editor friendly and easier to make data-driven. And with the decoupling, it's easier to organize a larger game project.

About performance, I don't know how expensive it will be . I will post the benchmark result when it's available.

S74nk0 commented 11 years ago

If vtable lookup causes high performance penalty maybe CRTP could be used, but that could also bring other problems since it is compile time polymorphism only

walzer commented 11 years ago

Well, 2 more bad samples.

  1. https://github.com/cocos2d/cocos2d-x/pull/2406.
  2. http://cocos2d-x.org/boards/18/topics/28085

CCObject was designed to only calculate reference counts, not for extending new features like acceptVisitor() or description(). It's a problem caused by object-orientated programming. Or maybe I should change CCObject to CCReference to clear its meaning.

totallyeviljake commented 11 years ago

we got rid of ccobject in cc2d-xna because it was only used for reference counting. I vote that you change the ccobject to ccreference to clear up the ambiguity.

sergey-shambir commented 11 years ago

About create function: overloaded new operator can eliminate this need. It also can do autorelease when user ask, something like auto sprite = new (kCCAutorelease) CCSprite or just auto sprite = new (true) CCSprite.

Another challenge is font issue when design size does not match device screen size, and their factor is not an integer. Fonts become blurry. In my local cocos2d-x version, i tweaked CCLabelTTF to use two sprites:

I've also tried to break CCLabelTTF->CCSprite inheritance, but it involved many changes in game code and finally didn't work as expected (and there was no way to fix it).

ricardoquesada commented 11 years ago

@walzer @totallyeviljake Creating the Reference object is a good idea. We could use C++ multiple inheritance if needed.

On the other hand, having a base object is useful in cocos2d-x. For example, adding the toString method to CCObject is useful, specially if you want to dump to the console the contents of a CCDictionary or CCArray. (I'm not a big fan of the visitor pattern that was added in cocos2d-x, I would prefer to have a simple toString or getDescription methods instead ). toString is also useful for debugging.

So, +1 for creating the CCReference object. But also we need to find a good way to dump objects in console. In C# and Objective-C this is not an issue, but we have to find a solution for cocos2d-x (C++)

sergey-shambir commented 11 years ago

(I'm not a big fan of the visitor pattern that was added in cocos2d-x, I would prefer to have a simple toString or getDescription methods instead ).

People also want to serialize dictionary/array to XML, JSON, Objective-C or java data structures. I cannot find faster and safer solution for type-dependend action in C++ than visitor.

Don't forget that in Objective-C you can cast NSDictionary* to NSArray* and call method "count" without problem. In C++ it's 100% crash, if count isn't virtual. Having virtual method for each particular task like safe "[id floatValue]", "[id intValue]", "[id boolValue]" is not scalable. So visitor also can be used when user isn't sure which data he parses.

Existing 3rd-party extension CCJSONSerialization produces CCBool and CCNull along with CCString, because in JSON null differs from string. No one can just ask "[id boolValue]" (in C++) and get correct answer, he will got crashes, crashes and yet more crashes.

ricardoquesada commented 11 years ago

@sergey-shambir +1 for finding a good way to dump objects... if the visitor pattern is the best one, lets use it. As I mentioned on the forum, if we are going to include the visitor pattern in CCObject then it should be used in the rest of the cocos2d-x code. eg, we should use it in CCDictionary # valueForKey as well: https://github.com/cocos2d/cocos2d-x/blob/cocos2d-2.1rc0-x-2.1.3/cocos2dx/cocoa/CCDictionary.cpp#L190

totallyeviljake commented 11 years ago

We tried to put in a base serializer to CCNode as well, but found it to be too heavy for the tree. Since not everyone wants to serialize their node structures, why impose the added code and support fields? We opted to put it all in a CCSerialization helper class. That doesn't work as well in C++ land though. Then again, nothing really works all that well for this stuff in C++ land.

You should all just convert to C# and be done. :) haha.

Didn't you C++ guys like to do a reference counted template class back in the old days? MyRefCounter {} or something? Maybe you could do something similar to that here - CCObject could become CCSerializable and then do all instances using MyRefCounter(). haha. just kidding again.

So @walzer is just going to change the name of the base from CCObject to something more interesting like CCSerializable or CCVisitorDescriptor, and then add another base, or as Ricardo comments, just use multi-inheritance to add in the CCReference for ref counting.

@sergey-shambir replacing the ::Create (self factory pattern) with new() is a huge undertaking. We only recently completed that with the help of Xamarin, and it took us about 6 weeks to clean all of that out. That's good work for a student!

Thinking about @sergey-shambir's comments about the JSON serializer, maybe the base serializer for CCObject should be a template class that allows for any hook to be added in there. So you can serialize to ccout, or to a JSON stream.

ricardoquesada commented 11 years ago

@totallyeviljake What would you suggest for C++ ?

Perhaps the visitor pattern suggested by @sergey-shambir is the best solution for this case, taking into account these two use cases:

  1. dump to string (debugging purposes )
  2. dump to JSON or optimized binary format

For 1., we don't need anything else. For 2., if we are going to add support for dumping objects into a binary format or JSON, we need a way to create the objects from the dumped data. Could we do that with a visitor pattern ? or will we need a new constructor for the objects ?

totallyeviljake commented 11 years ago

hmmm, been thinking about this - maybe the CCSerializer method we did may work for C++.

CCSerializer() .Instance = new (CCJSONSerializer | CCConsoleSerializer)

CCSerializer::Instance->toString(CCNode n); CCSerializer::Instance->description(CCNode n);

Something like that? At runtime, someone can set the serializer instance to push to their own format, but cc2d would just provide the default console serializer that dumps to ccout. You would still have to know about the insides of the node you are serializing though. There is no good way of doing that without making cc2d-x super slow.

ricardoquesada commented 11 years ago

@totallyeviljake Thanks. Did you mean CCSerialization ? Because I couldn't find a CCSerializer method in cocos2d-xna.

Boost also has a serializer object ( http://www.boost.org/doc/libs/1_37_0/libs/serialization/doc/index.html ). But as you can imagine, since C++ has not reflection, either you have to declare all your variables public, or add the serialize method in your classes. This approach is similar to the NSCoder protocol from Objective-C ( https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSCoder_Class/Reference/NSCoder.html#//apple_ref/doc/c_ref/NSCoder )

UPDATE: Another serialization library for C++: s11n http://s11n.net/

totallyeviljake commented 11 years ago

ha, yeah - CCSerialization, that's right, sorry. My head is in our scene generator project for cc2d-xna.

everything is always so much harder in C++. the only automatic way of generating serialization that I'd ever seen was back in the corba days using IDL and RPC compilers.

totallyeviljake commented 11 years ago

I like what boost has and their list of requirements. the versioning of data dumps is going to be important for the long term because someone will inevitably want to restore across revision boundaries. (laughing at the idea of me trying to restore my Moria save file from college not long ago)

No matter what, the library chosen will contaminate the source tree by requiring some interface implementation. That is the unfortunate side effect of using C++.

sergey-shambir commented 11 years ago

I still not sure which serialization feature requires interface definition. Did you mean user data records or standard cocos2d-x classes?

totallyeviljake commented 11 years ago

@sergey-shambir the serializer needs to know what methods to call, right?

CCObject : CCSerializationInterface {

public: void serializeToJSON(CCSerializerBase *cb); }

You could just ignore the interface and put it all into ccobject, but that's what @walzer is trying to refactor out of the base classes (and make them simpler).

Another way:

CCObject {

public: void setSerializer(CCSerializerBase cb); void serialize(); / no-op if no serializer is set */ }

As you pointed out earlier, we can't do a "generic" method of serialization and de-ser because the type conversions will end up with memory errors. So we end up doing a strongly typed ser like is done with the ccb file.

ricardoquesada commented 11 years ago

@totallyeviljake @all regarding the file format I would like to have a compact and super fast format ( https://github.com/cocos2d/cocos2d-js/issues/68 ) Let's say that CCNode has four 32-bit floats (x, y, rotation and scale ), so the way I envision the format would be something like:

4  32-bit floats: x, y, rotation and scale: how they are stored on disk
+----+----+----+----+
| x  | y  | r  | s  |
+----+----+----+----+

So the unmarshaller should know that the first 4 bytes belongs to X, the next four bytes belongs to Y... and so on. The rationale behind that format is that we should be able to restore a scene with 1.000+ nodes without problem. The parsing/loading time should be minimum.

Regarding making the nodes bigger due to the marshalling/unmarshalling code, we could add a compile time option that enables/disables that feature. If you know that your game won't need that feature, you can just disable it at compile time... although I don't think the marshalling/unmarshalling code will affect too much the memory.

totallyeviljake commented 11 years ago

can you ntoh() and hton() on a mobile device? I think the goal of the JSON serialization was to be able to store your serialization up on a server and then restore it over the web.

ricardoquesada commented 11 years ago

@totallyeviljake yes, you can use ntoh and hton in the device. The bytes should be stored in "network order" to prevent possible incompatibilities. JSON or text formats are good for certain things, but speed is not one of their strengths. If you need to use HTTP to send/get your saved game to/from the server you could uuencode/uudecode (or do base64) to your binary format and then send it using an HTTP push/get.

If needed we could also have a "debug" format that saves everything in JSON/text for debugging purposes.

If we could use a binary format with our own alloc library (C++ only I guess... not feasible on C# or Objective-C) we could load a scene of 1.000+ nodes in a few milliseconds (just a rough guess, assuming that the textures were already created )

totallyeviljake commented 11 years ago

excellent. so a packed serialization format is definitely the way to go. the graphs are going to be pretty big. This is a full game state save, like a full cocos2d core image? that's really cool if you can pull that off with a small binary footprint. Wish we could do that in C# land.

the debug format for human readability is a nice to have feature. It would make debugging problems much easier for @walzer (he just has to replay the graph once he gets the game code).

ricardoquesada commented 11 years ago

@totallyeviljake yes, that's the idea. To have a sort of game-state.cocos2d (or something like that), and it should be able to restore the whole state of your game: scene graph, textures, actions, etc...

A cocos2d-iphone user implemented a "whole cocos2d format" for the cocos2d-iphone v1.x branch: http://www.cocos2d-iphone.org/forum/topic/20528/page I think we could create something similar to that.

totallyeviljake commented 11 years ago

hey Ricardo, your link didn't work in your last post.

ricardoquesada commented 11 years ago

@totallyeviljake oops. This is the link: http://www.cocos2d-iphone.org/forum/topic/20528

totallyeviljake commented 11 years ago

FWIW. We are encountering this same dilemma in cocos2d-xna. Do we want a CCNode, CCInputNode, CCLayer, CCInputLayer, CCClippedLayer, etc.

Also, @walzer are you using ScissorRect on your CCLayer in cc2d-x? We found that bug recently in cc2d-xna and fixed it last night.

Also, you need to call SortAllChildren() in the Visit() in CCScrollView and any place that you override Visit() otherwise your children will not have a proper Zorder and the rendering will be messy. We fixed that today thanks to @gena-m.

ricardoquesada commented 11 years ago

@totallyeviljake Are you referring to the component based issue ? Let's define a good model for cocos2d v3.0.

Also, are you subscribed to the cocos2d-js-devel list ? https://groups.google.com/forum/?fromgroups#!forum/cocos2d-js-devel In that list we are discussing the new features for cocos2d 3.0.