Open walzer opened 11 years ago
Very useful info.
Does this issue deprecate issue #10 ?
@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.
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.
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.
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.
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.
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!
@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.
@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.
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
Well, 2 more bad samples.
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.
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.
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).
@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++)
(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.
@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
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
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.
@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:
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 ?
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.
@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/
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.
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++.
I still not sure which serialization feature requires interface definition. Did you mean user data records or standard cocos2d-x classes?
@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.
@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.
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.
@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 )
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).
@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.
hey Ricardo, your link didn't work in your last post.
@totallyeviljake oops. This is the link: http://www.cocos2d-iphone.org/forum/topic/20528
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.
@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.
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
Another bad sample is CCNode.h. It's too fat that includes almost everything in it, including:
onEnter()
,onExit()
series. Luckily we haven't put onTouch and onMouse events here.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
CCNode
to e.g.MyAudioSource
, for only use its position and scheduler. InMyAudioSource::update()
, I found the position of hero viathis->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 inMyLayer::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 likePhysicsComponentChipmunk
&PhysicsComponentBox2d
. We can haveonLevelClean
,onEnemyDied
,onSpaceshipHit
. This is important in C++ but not required in lua/javascript binding, scripts has reflection, they can handle callbacks better.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
References