Key Differences

Andy Dent edited this page Apr 10, 2017 · 20 revisions

CocosSharp is a new version of the venerable Cocos2D framework. It arrived to C# through a road that started with Objective-C (Cocos2D), then to C++ (Cocos2D-x), then to C# (Cocos2D-XNA).

While we set out to build CocosSharp, we wanted to create an idiomatic version of Cocos2D for the .NET environment. So we took Cocos2D-XNA, and we changed it in various ways. This document describes the changes

.NET Idioms

The original codebase had accumulated idioms and practices from the Objective-C, C++ and the original C# code base. The result was an API that had a few elements of .NET, but in general surfaced APIs, naming conventions and practices that were not in line with the recommendations of the .NET Framework Design Guidelines.

Some of the changes that we introduced include:

  • Protected members were using odd coding conventions, like having protected fields name "m_Foo", or "bEnabled". We treat all protected members as being part of the public API, and as such, they must follow the .NET conventions for members exposed in the API.

  • Removed hungarian notation naming on parameters: since parameters are part of the consumable API in C# (named parameters), we removed this oddity from the API (for example: void SetStatus (bool bEnabled)).

  • In many places, code used protected fields and public properties surfacing the fields. We have now opted for C#'s automatic property implementation and visibility qualifiers where appropriate on the properties.

  • Use C# constructors to initialize objects, and ensure proper chains of initialization. We got rid of the legacy set of methods from the Objective-C port that included several instances of the "initWith.." idiom, which could lead to half-constructed objects. We now let the compiler do the heavy lifting and ensure that objects are properly constructed and initialized.

Global State

One of the design decisions going back to Cocos2D was to have a singleton director object. While this works for applications whose sole purpose is being a single game, this poses problems when you consider having different parts of the applications host CocosSharp content (perhaps at different points in time, or perhaps having two windows on the Mac, each running an independent surface).

We have changed the design to not depend on this singleton, and instead, you can create as many directors as you application needs.

Actions as Immutable Recipes

Some of the code that we had inherited from Cocos2D was a model where Actions were objects that described changes to nodes on a scene. These objects were instantiated and would both be a recipe as well as being the state keepers of the action as it progressed.

A common idiom was to create a blueprint for the action that you desired, and in many cases, copies of this blueprint would be made. For example, move from point a to b, and then create a reverse action based on moving from point b to a. Another common idiom was to create an action for an object, then make a copy and attach the result to another object as well (for example, to have two objects track each other).

This resulted in cumbersome code, as there would be invocations to an interface method to clone an object, then typecasting would be involved, throwing away the safety of the .NET type system.

Internally, things were a lot worse. The copying idioms that had been accumulated over the years had give birth to two different implementation patterns, which were the source of difficult to diagnose bugs: sometimes a copy would create a new object, and sometimes it would populate an existing copy with the state from the object.

The code was messy, hard to debug, and hard to follow.

We opted for a simpler design. In CocosSharp, Actions are immutable recipes that describe an intent. They might describe the movement from point A to point B, but would never maintain the state of the object while it is moving from those two points. Instead, we made it so that Action objects create ActionState objects upon the action being initiated. These ActionState objects are transient, created on demand, and they are the only ones that track the state of the nodes that they affect. Actions themselves are immutable objects, and can easily be shared, and the entire notion of copying has been eliminated.

API Surface

We studied the API and reduced the API surface where possible. We removed utility and support classes from the public API as this allows us to change the internals without breaking compatibility in the future. By keeping the public contract smaller, we gain flexibility on what we can do internally.

We have also cleaned up the public API for fields, removed internal fields from the public API and sanitized both parameter names as well as some of the internals.

This helped pave the way for the Portable Class Libraries support in CocosSharp.

NuGet and Portable Class Libraries

We created a API surface area that is suitable to be a portable class library profile. This allows developers to consume CocosSharp from Portable Library Class projects, or to add the library without any compatibility concerns to a wide variety of platforms by simply installing a NuGet package.

This greatly benefits from the more compact API that is surfaced by CocosSharp.

For more information see NuGet Packages

Extensions

To help reduce the surface area of the core of CocosSharp, we have moved various non-core features to a separate CocosSharp extensions module. Those are components that developers can integrate into their projects, but the API might not be as comprehensive or as solidified as the rest of the API.

Sanitized Hierarchy

We have sanitized the hierarchy in CocosSharp to allow for future expansion into multiple windows, multiple displays and even multiple views of the same elements on the screen.

In the original Cocos2D family of APIs this is the hierarchy:

  • CCApplication (with singleton SharedApplication)
    • Initializes game
    • Handles input events
  • CCDirector (with singleton SharedDirector)
    • Handles window rendering
    • Maintains stack of CCScene objects
  • CCScene (sparse class, almost identical to CCNode)
    • Role not well defined
  • CCLayer (very similar to CCNode)
    • Introduces clipping when rendering
  • CCNode (base class for rendering an object)
    • Camera is optional
    • Camera is local, relative to the node's center

In the CocosSharp model, we broke away from this design, mostly to eliminate the need for the global SharedDirector and SharedApplication, to allow for CocosSharp content to exist in multiple windows (for example a desktop application) or to display different content on different UIScreens.

The model is now:

  • CCApplication (no longer a singleton, although limited to one instance when using current MonoGame)
    • Initializes game
    • Handles input events
  • CCWindow (no longer a singleton)
    • Takes over rendering (takes the role that previously was handled by CCDirector)
  • CCViewport (new!)
    • Wraps the MonoGame Viewport class
    • Adds support for multiple viewports
  • CCDirector
    • Solely responsible for maintaining a stack of CCScene objects
    • Previous tasks delegated to other classes
  • CCScene
    • Associates a group of CCNode children with a common CCWindow and a common CCViewport
  • CCLayer
    • Links a specific set of nodes to a common CCCamera
    • No longer resizable, takes on the dimensions of the viewport
  • CCNode
    • The existing workhorse

New hierarchy Use Cases

// Default case
// 
// Single director/viewport Layer's camera uses default visible bounds
// given by window design resolution Default viewport takes up entire
// window screen 
// 
public class AppDelegate : CCAppDelegate
{
	public override void AppDidFinishLaunching(CCApp app, CCWindow win)
	{
		win.SetDesignResolutionSize(960, 640, CCSceneResolutionPolicy.ShowAll);

		var scene = new CCScene(win);
		var layer = new MyLayer();

		scene.AddChild(layer);
		sharedWindow.RunWithScene(scene);
	}
}

// Custom camera
// 
// As above, with the exception that layer uses custom camera
//
public class AppDelegate : CCAppDelegate
{
	public override void AppDidFinishLaunching(CCApp app, CCWindow win)
	{
		win.SetDesignResolutionSize(960, 640, CCSceneResolutionPolicy.ShowAll);

		var scene = new CCScene(win);
		var layer = new MyLayer();

		var bounds = new CCSize(1000, 500);
		var target = new CCPoint3(0, 0, 3);
		var position = new CCPoint3(0, 0, 100);
		var camera = new CCCamera(bounds, position, target);

		layer.Camera = Camera;

		scene.AddChild(layer);
		sharedWindow.RunWithScene(scene);
	}
}

// Custom viewport
// 
// As with the default, with the exception that the scene uses a custom
// viewport
//
public class AppDelegate : CCAppDelegate
{
	public override void AppDidFinishLaunching(CCApp app, CCWindow win)
	{
		win.SetDesignResolutionSize(960, 640, CCSceneResolutionPolicy.ShowAll);

		// Specify the ratio of the screen to occupy
		// Note: top-left corner represents origin in screen space

		var topLeft = new CCViewport(new CCRect(0, 0, 0.5, 0.5));

		var scene = new CCScene(win, topLeft);

		// Make sure we set custom policy, or else we to default to policy
		// set by window i.e. CCSceneResolutionPolicy.ShowAll implies that
		// entire screen is used

		scene.SceneResolutionPolicy = CCSceneResolutionPolicy.Custom;

		var layer = new MyLayer();

		scene.AddChild(layer);
		sharedWindow.RunWithScene(scene);
	}
}

// Multiple running scenes/viewport
//
// Create multiple scene directors to have multiple running scenes
//
public class AppDelegate : CCAppDelegate
{
	public override void AppDidFinishLaunching(CCApp app, CCWindow win)
	{
		win.SetDesignResolutionSize(960, 640, CCSceneResolutionPolicy.ShowAll);

		var topLeft = new CCViewport(new CCRect(0, 0, .5, .5));
		var topRight = new CCViewport(new CCRect(0, 0, .5, .5));

		var sceneDirector1 = new CCDirector();
		var sceneDirector2 = new CCDirector();

		var scene1 = new CCScene(win, topLeft, sceneDirector1) {
			SceneResolutionPolicy = CCSceneResolutionPolicy.Custom
		};

		var scene2 = new CCScene(win, topRight, sceneDirector2) {
			SceneResolutionPolicy = CCSceneResolutionPolicy.Custom
		}

		var layer1 = new MyLayer();
		var layer2 = new MyLayer2();

		scene1.AddChild(layer1);
		scene2.AddChild(layer2);

		sceneDirector1.RunWithScene(scene1);
		sceneDirector2.RunWithScene(scene2);
	}
}

Coloring

Coloring has been folded into CCNode, eliminating various bugs that existed in half-implemented interfaces.

Events and Listeners

Events and Listeners have been rewritten from the original Delegate approach. There were problems originating from the fact that Events could be delivered out of sequence. With the incorporation of the event dispatching mechanism the events are now delivered in the order of the Scene Graph hierarchy using the node's Local and Global z-order.

Delivered Events:

  • CCEventListenerTouch - responds to touch events
  • CCEventListenerKeyboard - responds to keyboard events
  • CCEventListenerAcceleration - reponds to accelerometer events
  • CCEventListenMouse - responds to mouse events
  • CCEventListenerGamePad - responds to game pad events
  • CCEventListenerCustom - responds to custom events

Director generated custom events:

  • After Update
  • After Visit
  • After Draw
  • After Projection Change

Two types of Event Dispatching

  • Scene Graph
  • Fixed Priority

Unified Label

See the section on Labels

Greatly enhanced Tiled Tile Map Editor support

  • Very Large map loading and displaying
  • Tile culling - If the tile does not fit in the view it will not be drawn - enhanced performance
  • Support for Hexagonal and Staggered maps
  • Support for Tiled Animated tiles - Available in v0.11 Daily Download
  • Support for Objects - Rectangle, Ellipse, Line etc...
  • Custom properties

Licensing

CocosSharp is licensed under the MIT license terms, in a similar spirit to cocos2d for Objective-C and Cocos2d-x with C++. Cocos2D-XNA has switched from the MIT license to the Affero GPL license.