幻蓝博客 – 孤月蓝风

追寻互联网科技、Unity开发、AR/VR开发、游戏开发、Web前后端开发等技术。

使用CocosSharp制作一个游戏 – CocosSharp中文教程


注:本教程翻译自官方《Walkthrough – Building a game with CocosSharp》,官方教程有很多地方说的不够详细,或者代码不全,导致无法继续,本人在看了GoneBananas项目代码后,对本教程进行了部分修改,但当前只涉及Android方面,iOS因没有环境验证代码,暂未修改。

本人博客地址:http://fengyu.name

相关资源:

离线PDF文档:Download PDF

示例代码:Gone Bananas

相关链接:Cocos Sharp Samples,Cocos Sharp repo

本教程介绍了如何使用CocosSharp制作一个游戏,本教程将向你介绍如何使用CocosSharp制作一个游戏,CocosSharp是一个使用C#语言进行跨平台游戏开发的框架。

本教程展示了如何从建立一个项目到制作一个完整的游戏,以及CocosSharp中的各种概念,和如何使用它们创建游戏。CocosSharp概念包括:sprites(精灵),actions(动作),sounds(声音),layers(层),parallas(视差),particle system(粒子系统),scenes(场景)以及physics(物理)。

当你学习完后,你将已创建你的第一个CocosSharp游戏。

获得 CocosSharp

CocosSharp作为NuGet的一个PCL(Portable Class Library便携类库)存在,CocosSharp的源代码可以在GitHub上找到。

制作向导

在本教程中,你将建立一个名字叫做GoneBananas的游戏,这个游戏的目标是让猴子在屏幕上移动,并尽可能多的接到屏幕上向下掉的香蕉。游戏截图如下所示:

在本教程中我们将创建一个iOS及Android游戏。当然,CocosSharp也同样可以在许多其他平台良好运行。查看CocosSharp repo获得支持平台的完整列表。

本教程的第一部分,我们将介绍一些基本的操作,使用sprites,并让其显示在屏幕中。然后,我们将会介绍更多的高级概念以及它们如何使用,例如粒子系统与物理引擎。

创建项目

使用iOS->iPhone Empty Project模板,创建一个名为GoneBananas的新项目。

创建了项目之后,我们在解决方案中添加CocosSharp以及相关依赖文件。我们可以**从NuGet添加CocosSharp包**。

同样,使用Android->Ice Cream Sandwich Application模板新建一个名为GoneBananasAndroid的Android项目,并**添加CocosSharp的NuGet包**。

我们将使用一个共享项目来实现游戏逻辑,所以游戏逻辑将是跨平台可重用的。在解决方案中新建一个名为GonesBananasShared的共享项目。

之后,我们将资源文件夹复制到GoneBananas项目目录中,Android下,需要复制到Assets目录中。资源文件夹包含资源文件,例如字体、图片和声音。

上述工作结束后,我们创建游戏的准备工作就都完成了。

创建应用程序委托类Application Delegate

首先,我们需要创建CCApplicationDelegate的子类,CCApplicationDelegate的概念类似iOS中的UIApplicationDelegate。

这是事件用于控制程序的生命周期,大纲如下:

  • ApplicationDidFinishLaunching – 程序启动后执行

  • ApplicationDidEnterBackground – 当程序进入后台时,停止所有运行中的动画及音频

  • ApplicationWillEnterForeground – 当游戏恢复前台时,恢复所有因进入后台状态而被停止的动画及音频。

我们先将整个content文件夹复制到GoneBananas项目目录中,Android下,需要复制到Assets目录中。这个文件夹包含了资源文件,例如字体、图片和声音。

使用下面的代码新增一个名为GoneBananasApplicationDelegate的类:

using CocosDenshion;
using CocosSharp;

namespace GoneBananas
{
    public class GoneBananasApplicationDelegate : CCApplicationDelegate
    {

        public override void ApplicationDidFinishLaunching (CCApplication application, CCWindow mainWindow)
        {
            //关闭多重采样
            application.PreferMultiSampling = false;
            //默认文件目录
            application.ContentRootDirectory = "Content";
            //设置屏幕显示方式(横屏或竖屏)
            mainWindow.SupportedDisplayOrientations = CCDisplayOrientation.Portrait;
            //添加文件搜索目录
            application.ContentSearchPaths.Add ("hd");
            //音频引擎加载声音文件
            CCSimpleAudioEngine.SharedEngine.PreloadEffect ("Sounds/tap");
            //创建场景
            CCScene scene = GameStartLayer.GameStartLayerScene (mainWindow);
            //运行场景
            mainWindow.RunWithScene (scene);
        }

        public override void ApplicationDidEnterBackground (CCApplication application)
        {
            // stop all of the animation actions that are running.
            application.Paused = true;

            // if you use SimpleAudioEngine, your music must be paused
            CCSimpleAudioEngine.SharedEngine.PauseBackgroundMusic ();
        }

        public override void ApplicationWillEnterForeground (CCApplication application)
        {
            application.Paused = false;

            // if you use SimpleAudioEngine, your background music track must resume here. 
            CCSimpleAudioEngine.SharedEngine.ResumeBackgroundMusic ();
        }
    }
}

GoneBananasApplicationDelegateApplicationFinishedLaunching方法中,我们设置了application.ContentRootDirectorycontent文件夹,并对mainWindow进行了初始化,使其默认以竖屏状态运行,之后预加载了一个音频特效,并创建了场景实例已经调用他。但当前我们并没有创建GameStartLayer类,这个会在之后创建。

CCApplication类

CCApplicaiton类用来开始游戏,我们需要在不同平台项目中都创建它。这是唯一在各种平台具体项目中都需要的代码。

在iOS中,使用以下代码替换AppDelegade内容:

using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using CocosSharp;

namespace GoneBananas
{
    [Register ("AppDelegate")]
    public partial class AppDelegate : UIApplicationDelegate
    {
        public override void FinishedLaunching (UIApplication app)
        {
            var application = new CCApplication ();
            application.ApplicationDelegate = new GoneBananasApplicationDelegate ();
            application.StartGame ();
        }
    }
}

同样,对于Android,使用以下代码替换MainActivity的内容:

using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using CocosSharp;
using Microsoft.Xna.Framework;
using GoneBananas;

namespace GoneBananasAndroid
{
    [Activity(
        Label = "GoneBananas",
        AlwaysRetainTaskState = true,
        Icon = "@drawable/ic_launcher",
        Theme = "@android:style/Theme.NoTitleBar",
        ScreenOrientation = ScreenOrientation.Portrait,
        LaunchMode = LaunchMode.SingleInstance,
        MainLauncher = true,
        ConfigurationChanges =  ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden)
    ]
    public class MainActivity : AndroidGameActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            var application = new CCApplication();
            application.ApplicationDelegate = new GoneBananasApplicationDelegate();
            SetContentView(application.AndroidContentView);
            application.StartGame();
        }
    }
}

创建第一个场景

之前在GoneBananasApplicationDelegate类中,我们使用下面的代码创建第一个场景的实例:

CCScene scene = GameStartLayer.GameStartLayerScene (mainWindow);

CocosSharp使用在CCScene类中实现的scenes场景,用来管理游戏中各种各样的逻辑。每一个scene场景都包含layers层,而layer层用来显示在scene中的用户界面。每一个layer层都用来返回他的父场景。例如,在游戏中,我们创建三个层:

  • GameStartLayer – 提供引导界面,点击后将开始游戏

  • GameLayer – 游戏内容层

  • GameOverLayer –游戏结束时显示的界面,点击后将重新开始游戏

我们使用以下代码新建一个GameStartLayer类:

using System;
using System.Collections.Generic;
using CocosSharp;

namespace GoneBananas
{
    public class GameStartLayer : CCLayerColor
    {
        public GameStartLayer () : base ()
        {
            var touchListener = new CCEventListenerTouchAllAtOnce ();
            touchListener.OnTouchesEnded = (touches, ccevent) => Window.DefaultDirector.ReplaceScene (GameLayer.GameScene (Window));

            AddEventListener (touchListener, this);

            Color = CCColor3B.Black;
            Opacity = 255;
        }

        protected override void AddedToScene ()
        {
            base.AddedToScene ();

            Scene.SceneResolutionPolicy = CCSceneResolutionPolicy.ShowAll;

            var label = new CCLabelTtf ("Tap Screen to Go Bananas!", "arial", 22) {
                Position = VisibleBoundsWorldspace.Center,
                Color = CCColor3B.Green,
                HorizontalAlignment = CCTextAlignment.Center,
                VerticalAlignment = CCVerticalTextAlignment.Center,
                AnchorPoint = CCPoint.AnchorMiddle,
                Dimensions = ContentSize
            };

            AddChild (label);
        }

        public static CCScene GameStartLayerScene (CCWindow mainWindow)
        {
            var scene = new CCScene (mainWindow);
            var layer = new GameStartLayer ();

            scene.AddChild (layer);

            return scene;
        }
    }
}

我们使用一个CCLayerColor作为基础类,所以我们可以直接设置层的背景颜色。层中显示一个文本框,并将在点击屏幕后切换到GameLayer层界面。这个文本框使用了我们之前复制到content文件夹中fonts文件夹下的arial字体。

下面的截图展示完成后的场景:

切换到GameLayer场景

调用Window.DefaultDirector.ReplaceScene,在当前场景与其他场景之间切换。使用以下代码来执行切换到GameLayer场景:

Window.DefaultDirector.ReplaceScene (GameLayer.GameScene (Window));

执行GameLayer场景

对于GameLayer,创建一个从CCLayerColor继承名为GameLayer的类。

我们在本教程中自始至终的使用一个多种类变量,让我们继续,并创建他们:

using System;
using System.Collections.Generic;
using CocosDenshion;
using CocosSharp;
using System.Linq;

using Box2D.Common;
using Box2D.Dynamics;
using Box2D.Collision.Shapes;

namespace GoneBananas
{
    public class GameLayer : CCLayerColor
    {
        const float MONKEY_SPEED = 350.0f;
        const float GAME_DURATION = 60.0f; // game ends after 60 seconds or when the monkey hits a ball, whichever comes first

        // point to meter ratio for physics
        const int PTM_RATIO = 32;

        float elapsedTime;
        CCSprite monkey;
        List<CCSprite> visibleBananas;
        List<CCSprite> hitBananas;

        // monkey walking animation
        CCAnimation walkAnim;
        CCRepeatForever walkRepeat;
        CCCallFuncN walkAnimStop = new CCCallFuncN (node => node.StopAllActions ());

        // background sprite
        CCSprite grass;

        // particles
        CCParticleSun sun;

        // circle layered behind sun
        CCDrawNode circleNode;

        // parallax node for clouds
        CCParallaxNode parallaxClouds;

        // define our banana rotation action
        CCRotateBy rotateBanana = new CCRotateBy (0.8f, 360);

        // define our completion action to remove the banana once it hits the bottom of the screen
        CCCallFuncN moveBananaComplete = new CCCallFuncN (node => node.RemoveFromParent ());

        ...
    }
}

添加一个背景sprite精灵

一个sprite精灵,是一个已经使用addChild添加到layer节点中的CCSprite实例。

背景sprite精灵是从content文件夹中加载的一个文件。继续在GameLayer中添加以下代码用来创建背景:

using System;
using System.Collections.Generic;
using CocosDenshion;
using CocosSharp;
using System.Linq;

using Box2D.Common;
using Box2D.Dynamics;
using Box2D.Collision.Shapes;

namespace GoneBananas
{
    public class GameLayer : CCLayerColor
    {
        ...
        void AddGrass ()
        {
            grass = new CCSprite ("grass");
            AddChild (grass);
        }
        ...
    }
}

创建香蕉sprites精灵

每一个从屏幕上往下掉的香蕉都是通过一个图片文件创建的sprite精灵。香蕉图片文件被包含在一个sprite sheet精灵表中。一个精灵表是包含了多个图片文件的集合,他们会被更高效的加载。一个plist文件包含了具体图片的信息,例如香蕉,就是在sprite sheet精灵表中。

使用以下代码来创建一个名为AddBanana的方法,用来创建香蕉sprite并显示在层中,添加位置同上,与AddGrass处于同级:

void AddGrass ()
{
    ...
}

//以上为之前添加的AddGrass
//AddBanana与AddGrass处于同级

CCSprite AddBanana ()
{
    var spriteSheet = new CCSpriteSheet ("animations/monkey.plist");
    var banana = new CCSprite (spriteSheet.Frames.Find ((x) => x.TextureFilename.StartsWith ("Banana")));

    var p = GetRandomPosition (banana.ContentSize);
    banana.Position = p;
    banana.Scale = 0.5f;

    AddChild (banana);

    var moveBanana = new CCMoveTo (5.0f, new CCPoint (banana.Position.X, 0));
    banana.RunActions (moveBanana, moveBananaComplete);
    banana.RepeatForever (rotateBanana);

    return banana;
}

这些代码在屏幕的一个随机x位置添加一个sprite精灵,让我们看看如何使每一个sprite精灵从屏幕上向下移动直到在屏幕底部消失。

使用Actions来控制香蕉

CocosSharp包含了各种CCAction类,用来在一个节点上执行不同的任务,包含了继承自CCNodeCCSprite实例sprite。

在这个示例中,我们将使用CCMoveTo动作来控制香蕉sprite,并在动画执行结束后使用CCCallFuncN移除精灵。

我们使用包含各种动作的CCSequence来完成它,他本身也是一个动作包含了不同的动作来继续执行。

将以下代码添加到AddBanana方法中,并放置在返回香蕉sprite之前:

CCSprite AddBanana ()
{
    var spriteSheet = new CCSpriteSheet ("animations/monkey.plist");
    var banana = new CCSprite (spriteSheet.Frames.Find ((x) => x.TextureFilename.StartsWith ("Banana")));

    var p = GetRandomPosition (banana.ContentSize);
    banana.Position = p;
    banana.Scale = 0.5f;

    AddChild (banana);

    //这里是放置的位置
    var moveBanana = new CCMoveTo (5.0f, new CCPoint (banana.Position.X, 0));
    banana.RunActions (moveBanana, moveBananaComplete);
    banana.RepeatForever (rotateBanana);

    return banana;
}

添加猴子sprite

接下来,在Gamelayer中添加下面的构造代码:

public GameLayer ()
{
    var touchListener = new CCEventListenerTouchAllAtOnce ();
    touchListener.OnTouchesEnded = OnTouchesEnded;

    Color = new CCColor3B (CCColor4B.White);
    Opacity = 255;

    visibleBananas = new List<CCSprite> ();
    hitBananas = new List<CCSprite> ();

    AddGrass ();
    AddSun (); // we'll implement this later using a particle system
    AddMonkey ();

}

我们现在要做的,就是让游戏用visibleBananashitBananas来管理得分及碰撞检测,猴子sprite同样被添加到sprite sheet精灵表中,仅仅猴子,我们就需要用一些图片来控制猴子的走动,继续在GameLayer中添加代码:

void AddMonkey ()
{
    var spriteSheet = new CCSpriteSheet ("animations/monkey.plist");
    var animationFrames = spriteSheet.Frames.FindAll ((x) => x.TextureFilename.StartsWith ("frame"));

    walkAnim = new CCAnimation (animationFrames, 0.1f);
    walkRepeat = new CCRepeatForever (new CCAnimate (walkAnim));
    monkey = new CCSprite (animationFrames.First ()) { Name = "Monkey" };
    monkey.Scale = 0.25f;

    AddChild (monkey);
}

在点击后移动猴子sprite

用户点击屏幕来控制猴子sprite移动到当前点击的位置,我们重载了TouchesEnded并创建了另一个动作MoveToGameLayer中添加如下代码:

void OnTouchesEnded (List<CCTouch> touches, CCEvent touchEvent)
{
    monkey.StopAllActions ();

    var location = touches [0].LocationOnScreen;
    location = WorldToScreenspace (location);
    float ds = CCPoint.Distance (monkey.Position, location);

    var dt = ds / MONKEY_SPEED;

    var moveMonkey = new CCMoveTo (dt, location);

    monkey.RunAction (walkRepeat);
    monkey.RunActions (moveMonkey, walkAnimStop);
}

碰撞检测

对于猴子和香蕉之间的碰撞检测,我们简单的为每一个sprite做一个bounding box弹力盒检测。我们同样保持跟踪香蕉被猴子碰撞的顺序,然后从layer中移除他们,并保持分数。

以下代码用来检查碰撞,添加到GameLayer中:

void CheckCollision ()
{
    visibleBananas.ForEach (banana => {
        bool hit = banana.BoundingBoxTransformedToParent.IntersectsRect (monkey.BoundingBoxTransformedToParent);
        if (hit) {
            hitBananas.Add (banana);
            CCSimpleAudioEngine.SharedEngine.PlayEffect ("Sounds/tap");
            Explode (banana.Position); // we'll implement this later using a particle systtem
            banana.RemoveFromParent ();
        }
    });

    hitBananas.ForEach (banana => visibleBananas.Remove (banana));
}

调度

在固定的重复间隔中执行代码,按顺序的添加香蕉、检查碰撞并测试游戏是否结束的逻辑,我们可以使用调度器方法,像一个timer一样的动作。

我们可以用我们之前创建的构造器来调度我们希望结束时执行的代码,仍然是在GameLayer中,代码放在public GameLayer()中:

...
namespace GoneBananas
{
    public class GameLayer : CCLayerColor
    {
        ...
        public GameLayer ()
        {
            ...
            //这一段是需要添加的代码
            Schedule (t => {
                visibleBananas.Add (AddBanana ());
                elapsedTime += t;
                if (ShouldEndGame ()) {
                    EndGame ();
                }
            }, 1.0f);
            Schedule (t => CheckCollision ());
            ...
        }
        ...
    }
}

播放一个声音特效

让我们在猴子和香蕉碰撞后播放一个声音特效。文件tap.mp3被放在content/sounds文件夹中。要播放一个声音特效,我们使用CCSimpleAudioEngine类,添加以下的代码到TouchesEnded方法的结尾:

void OnTouchesEnded (List<CCTouch> touches, CCEvent touchEvent)
{
    ...
    //需要添加的代码
    CCSimpleAudioEngine.SharedEngine.PlayEffect ("Sounds/tap");
}

注:官方教程中,让将这段代码添加到TouchesEnded方法的结尾,但我再查看Demo源码时,发现其加在了碰撞检测CheckCollision事件中。

添加游戏结束逻辑

我们还需要一些方法来结束游戏。例子中,我们简单的在60秒后结束游戏:

bool ShouldEndGame ()
{
    return elapsedTime > GAME_DURATION;
}

void EndGame ()
{
    var gameOverScene = GameOverLayer.SceneWithScore (Window, hitBananas.Count);
    var transitionToGameOver = new CCTransitionMoveInR (0.3f, gameOverScene);
    Director.ReplaceScene (transitionToGameOver);
}

使用视差添加云朵

CocosSharp包含了一个CCParallaxNode,我们可以用来添加sprite并实现不同相对速度的视差效果。

添加以下来代码来实现猴子运动时,云朵与其产生的不同相对速度的视差效果:

void AddClouds ()
{
    float h = VisibleBoundsWorldspace.Size.Height;

    parallaxClouds = new CCParallaxNode {
        Position = new CCPoint (0, h)
    };

    AddChild (parallaxClouds);

    var cloud1 = new CCSprite ("cloud");
    var cloud2 = new CCSprite ("cloud");
    var cloud3 = new CCSprite ("cloud");

    float yRatio1 = 1.0f;
    float yRatio2 = 0.15f;
    float yRatio3 = 0.5f;

    parallaxClouds.AddChild (cloud1, 0, new CCPoint (1.0f, yRatio1), new CCPoint (100, -100 + h - (h * yRatio1)));
    parallaxClouds.AddChild (cloud2, 0, new CCPoint (1.0f, yRatio2), new CCPoint (250, -200 + h - (h * yRatio2)));
    parallaxClouds.AddChild (cloud3, 0, new CCPoint (1.0f, yRatio3), new CCPoint (400, -150 + h - (h * yRatio3)));
}

添加粒子系统

CocosSharp包含了一些粒子系统用来创建多样的、让人感兴趣的特效,例如fire火、smoke烟、sun太阳和explosion爆炸等等。

在GoneBananas中,让我们在猴子碰撞香蕉时添加一个爆炸效果:

void Explode (CCPoint pt)
{
    var explosion = new CCParticleExplosion (pt);
    explosion.TotalParticles = 10;
    explosion.AutoRemoveOnFinish = true;
    AddChild (explosion);
}

同样,在右上方我们添加一个太阳,并拥有一个微妙的特效。

void AddSun ()
{
    circleNode = new CCDrawNode ();
    circleNode.DrawSolidCircle (CCPoint.Zero, 30.0f, CCColor4B.Yellow);
    AddChild (circleNode);

    sun = new CCParticleSun (CCPoint.Zero);
    sun.StartColor = new CCColor4F (CCColor3B.Red);
    sun.EndColor = new CCColor4F (CCColor3B.Yellow);
    AddChild (sun);
}

初始化场景

我们同样需要初始化一些我们已经实现的东西,例如初始化sprite的位置。我们可以用AddedToScene来实现这些,代码如下:

protected override void AddedToScene ()
{
    base.AddedToScene ();

    Scene.SceneResolutionPolicy = CCSceneResolutionPolicy.NoBorder;

    grass.Position = VisibleBoundsWorldspace.Center;
    monkey.Position = VisibleBoundsWorldspace.Center;

    var b = VisibleBoundsWorldspace;
    sun.Position = b.UpperRight.Offset (-100, -100);

    circleNode.Position = sun.Position;

    AddClouds ();
}

创建场景

我们需要提供一种途径让layer返回场景,就像我们之前做的GameStartLayer。添加下面的静态属性来完成这些工作:

public static CCScene GameScene (CCWindow mainWindow)
{
    var scene = new CCScene (mainWindow);
    var layer = new GameLayer ();

    scene.AddChild (layer);

    return scene;
}

添加物理效果

让我们来用physics来添加一个特性让游戏更真实一些。我们使用Box2D的C#接口,来添加一些乱跳的球到游戏中。它用来避免球碰撞到香蕉。

我们需要一对类变量,用来各自控制物理世界和球sprite:

...
namespace GoneBananas
{
    ...
    public class GameLayer : CCLayerColor
    {
        ...
        // physics world
        b2World world;

        // balls sprite batch
        CCSpriteBatchNode ballsBatch;
        CCTexture2D ballTexture;
    }
}

然后我们可以在构造器中添加代码为球sprite创建一些节点。代码放置在public GameLayer ()中:

...
namespace GoneBananas
{
    public class GameLayer : CCLayerColor
    {
        ...
        public GameLayer ()
        {
            ...
            //需要添加的代码
            // batch node for physics balls
            ballsBatch = new CCSpriteBatchNode ("balls", 100);
            ballTexture = ballsBatch.Texture;
            AddChild (ballsBatch, 1, 1);
            ...
        }
    }
}

一个CCSpriteBatchNode同时渲染所有的sprites,更有效的利用GPU。接下来,添加下面的代码用来初始化物理世界以及添加球sprite:

void InitPhysics ()
{
    CCSize s = Layer.VisibleBoundsWorldspace.Size;

    var gravity = new b2Vec2 (0.0f, -10.0f);
    world = new b2World (gravity);

    world.SetAllowSleeping (true);
    world.SetContinuousPhysics (true);

    var def = new b2BodyDef ();
    def.allowSleep = true;
    def.position = b2Vec2.Zero;
    def.type = b2BodyType.b2_staticBody;
    b2Body groundBody = world.CreateBody (def);
    groundBody.SetActive (true);

    b2EdgeShape groundBox = new b2EdgeShape ();
    groundBox.Set (b2Vec2.Zero, new b2Vec2 (s.Width / PTM_RATIO, 0));
    b2FixtureDef fd = new b2FixtureDef ();
    fd.shape = groundBox;
    groundBody.CreateFixture (fd);
}

void AddBall ()
{
    int idx = (CCRandom.Float_0_1 () > .5 ? 0 : 1);
    int idy = (CCRandom.Float_0_1 () > .5 ? 0 : 1);
    var sprite = new CCPhysicsSprite (ballTexture, new CCRect (32 * idx, 32 * idy, 32, 32), PTM_RATIO);

    ballsBatch.AddChild (sprite);

    CCPoint p = GetRandomPosition (sprite.ContentSize);

    sprite.Position = new CCPoint (p.X, p.Y);

    var def = new b2BodyDef ();
    def.position = new b2Vec2 (p.X / PTM_RATIO, p.Y / PTM_RATIO);
    def.type = b2BodyType.b2_dynamicBody;
    b2Body body = world.CreateBody (def);

    var circle = new b2CircleShape ();
    circle.Radius = 0.5f;

    var fd = new b2FixtureDef ();
    fd.shape = circle;
    fd.density = 1f;
    fd.restitution = 0.85f;
    fd.friction = 0.3f;
    body.CreateFixture (fd);

    sprite.PhysicsBody = body;

    Console.WriteLine ("sprite batch node count = {0}", ballsBatch.ChildrenCount);
}

public override void OnEnter ()
{
    base.OnEnter ();

    InitPhysics ();
}

还需要添加一个碰撞检测在CheckCollision方法中,用来决定当猴子碰撞到球后的事件:

void CheckCollision ()
{
    ...
    hitBananas.ForEach (banana => visibleBananas.Remove (banana));

    int ballHitCount = ballsBatch.Children.Count (ball => ball.BoundingBoxTransformedToParent.IntersectsRect (monkey.BoundingBoxTransformedToParent));

    if (ballHitCount > 0) {
        EndGame ();
    }
}

最终,我们需要在之前为了添加香蕉时创建的调度器中调用AddBall事件,也需要添加一个额外的调度器用来控制物理世界的步长,将下面的代码添加至public GameLayer()的末尾:

...
namespace GoneBananas
{
    public class GameLayer : CCLayerColor
    {
        ...
        public GameLayer ()
        {
            ...
            //需要添加的代码
            Schedule (t => {
                visibleBananas.Add (AddBanana ());
                elapsedTime += t;
                if (ShouldEndGame ()) {
                    EndGame ();
                }
                AddBall ();
            }, 1.0f);

            Schedule (t => {
                world.Step (t, 8, 1);

                foreach (CCPhysicsSprite sprite in ballsBatch.Children) {
                    if (sprite.Visible && sprite.PhysicsBody.Position.x < 0f || sprite.PhysicsBody.Position.x * PTM_RATIO > ContentSize.Width) {
                        world.DestroyBody (sprite.PhysicsBody);
                        sprite.Visible = false;
                        sprite.RemoveFromParent ();
                    } else {
                        sprite.UpdateTransformedSpriteTextureQuads ();
                    }
                }
            });
        }
    }
}

现在我们就可以运行游戏了。

实现GameOverLayer层

在GameLayer中我们添加了一些代码用来在游戏结束时切换到GameOverLayer场景。GameOverLayerGameStartLayer非常的相似,主要的区别是已经获取到了用来显示的游戏分数的文本框的值。同样,当用户点击GameOverLayer后,将会开始一个新游戏。使用如下的代码添加一个名为GameOverLayer的类:

using System;
using System.Collections.Generic;
using CocosSharp;

namespace GoneBananas
{
    public class GameOverLayer : CCLayerColor
    {

        string scoreMessage = string.Empty;

        public GameOverLayer (int score) //: base(new CCSize (640, 1136))
        {

            var touchListener = new CCEventListenerTouchAllAtOnce ();
            touchListener.OnTouchesEnded = (touches, ccevent) => Window.DefaultDirector.ReplaceScene (GameLayer.GameScene (Window));

            AddEventListener (touchListener, this);

            scoreMessage = String.Format ("Game Over. You collected {0} bananas!", score);

            Color = new CCColor3B (CCColor4B.Black);

            Opacity = 255;
        }

        public void AddMonkey ()
        {
            var spriteSheet = new CCSpriteSheet ("animations/monkey.plist");
            var frame = spriteSheet.Frames.Find ((x) => x.TextureFilename.StartsWith ("frame"));

            var monkey = new CCSprite (frame) {
                Position = new CCPoint (VisibleBoundsWorldspace.Size.Center.X + 20, VisibleBoundsWorldspace.Size.Center.Y + 300),
                Scale = 0.5f
            };

            AddChild (monkey);
        }

        protected override void AddedToScene ()
        {
            base.AddedToScene ();

            Scene.SceneResolutionPolicy = CCSceneResolutionPolicy.ShowAll;

            var scoreLabel = new CCLabelTtf (scoreMessage, "arial", 22) {
                Position = new CCPoint (VisibleBoundsWorldspace.Size.Center.X, VisibleBoundsWorldspace.Size.Center.Y + 50),
                Color = new CCColor3B (CCColor4B.Yellow),
                HorizontalAlignment = CCTextAlignment.Center,
                VerticalAlignment = CCVerticalTextAlignment.Center,
                AnchorPoint = CCPoint.AnchorMiddle,
                Dimensions = ContentSize
            };

            AddChild (scoreLabel);

            var playAgainLabel = new CCLabelTtf ("Tap to Play Again", "arial", 22) {
                Position = VisibleBoundsWorldspace.Size.Center,
                Color = new CCColor3B (CCColor4B.Green),
                HorizontalAlignment = CCTextAlignment.Center,
                VerticalAlignment = CCVerticalTextAlignment.Center,
                AnchorPoint = CCPoint.AnchorMiddle,
                Dimensions = ContentSize
            };

            AddChild (playAgainLabel);

            AddMonkey ();
        }

        public static CCScene SceneWithScore (CCWindow mainWindow, int score)
        {
            var scene = new CCScene (mainWindow);
            var layer = new GameOverLayer (score);

            scene.AddChild (layer);

            return scene;
        }
    }
}

最终的GameOverLayer场景如下图所示:

你可以在模拟器或设备中运行游戏并玩一下。

现在恭喜你,你成功的使用CocosSharp创建了你的第一个游戏。

总结

在本篇教程中,我们使用CocosSharp创建了一个游戏。我们创建了一些场景用来显示游戏及控制游戏逻辑。在这个过程中,我们覆盖到了如何添加sprite并用actions控制他们、手势点击、添加声音特效和在场景中进行切换。我们还添加了粒子系统和视差效果用来包含一些令人感兴趣的可是特效,并将物理模拟让游戏变得更加接近真实。


2DToolkit官方文档中文版打地鼠教程(十二):添加文本


我们的游戏已经可以玩了。地鼠从洞里钻出来,打中他们后,会消失在一朵烟里。但你想知道你游戏玩得怎么样,就需要在屏幕上显示分数。

我们使用2D Toolkit中的一个演示字体,你也可以使用自己喜欢的字体包

在我们添加分数文本之前,我们需要确保他能正确显示在屏幕上。我们添加一个anchor在我们的摄像机中,让分数文本能够更简单的定位。

  1. 在Hierarchy窗口,选中摄像机对象,并点击Create > tk2d > Camera Anchor。

  2. 在Hierarchy窗口中,一个Anchor对象被添加为摄像机的子对象。

  3. 我将把分数文本显示在游戏窗口的右上角,所以让场景的anchor到右上角。因此在Inspector窗口中的Tk 2d Camera Anchor(Script)的下面,点击Anchor下拉列表并选中UpperRight。注意设置完之后anchior的x和y会被设置为摄像机的宽和高,那就是屏幕的右上角。

  4. 我们的分数文本的位置将会相对于anchor,在Hierarchy窗口中选中Anchor,然后点击Create > tk2d > TextMesh。

  5. 在Hierarchy窗口中选中TextMesh,在Inspector窗口中你可以改变它的各种设置,这里需要修改的仍然是Anchor锚点。由于文本默认从左到右显示,如果我们选择TopRight,那么文本在屏幕上的位置将会改变。在分数变得更大时,为了容纳更多文本,它会变得更大。所以为了使分数显示在同一个地方,我们选择Upper Left作为Anchor设置。

    从字体下拉列表中选择GradientFont,并设置它的Max Chars值,这里设置为11,这包括了Score这个词和后面的分数。注意,MaxChars值设置过大会浪费内存,设置过小会导致文本被裁剪。在文本框中输入一些文本,我们预览文本的显示效果,随后我们会在脚本中控制文本内容。

    在TextureGradient中选择一个你喜欢的效果,我这里选择desert-horizon。

    默认字体很小,所以我们需要对文本进行缩放以显示在屏幕上适合的大小,我们这里选择 600 * 500。

  6. 现在,我们有了显示分数的文本,我们需要能够改变分数文本。在Project窗口中,创建一个C#脚本命名为ScoreScript,并将这里的代码粘贴进去。ScoreScript让我们能够很简单的获取及修改current score分数值。

  7. 在Project窗口中,选中ScoreScript并将它拖动到Hierarchy窗口中的ScoreText对象上。脚本将会被到添加到score文本对象中。

  8. 当打中一个地鼠后,增加10分,但没有打中时,扣去5分。变量和函数我们已经写在了脚本中,ScoreScript类使用它时并不需要创建实例,我们可以简单的使用:

    ScoreScript.Score += 10;

    来更新分数。 因此当打中地鼠后( if(mole.sprite.gameObject.activeSelf && mole.ColliderTransform == hit.transform) ),我们通过在MainGameScript增加下面的代码来控制分数。我们将使用的方法的核心代码是:

    ScoreScript.Score += mole.Whacked ? 0 : 10;

    如果洞中的地鼠已被打中过,则0分。


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(十三):为游戏添加声音


现在我们为游戏添加一些音效。当地鼠被打中、从洞里钻出以及钻回洞中时会播放。我们在Projects > Mole > Sounds中已经有了这些音效。关于可用和不可用的音效格式,你可以查看这里

将声音片段添加到游戏中很容易!

  1. 取消每一个音效的3D Sound选项。

  2. 为beat_mole音效在MainGameScript添加一个AudioClip变量:

    public AudioClip moleHit;

  3. 然后在打中地鼠的代码中,也就是分数增加的代码,增加如下代码:

    AudioSource.PlayClipAtPoint(moleHit, new Vector3());

    此函数接受一个AudioClip和Vector3作为参数。Vector3对3D音效有效果,但我们这里使用的2D声音,所以我们只新建一个空的Vector3。

  4. 现在在Hierarchy窗口中选中MainGameScript,将会出现一个MoleHit音效字段,我们可以将音效拖到上面。
    img/gamescript_audio_clip.png

  5. 添加mole_down和mole_up音效,在MoleScript脚本下声明变量。

    public AudioClip moleUp;

    public AudioClip moleDown;

  6. 在MoveUp函数的最开始,循环之前,添加以下代码:

    AudioSource.PlayClipAtPoint(moleUp, new Vector3());

    在MoveDown函数最结尾,循环之后,添加以下代码:

    AudioSource.PlayClipAtPoint(moleDown, new Vector3());

  7. 同样,如果我们选中MoleUnit,在Inspector窗口中的MoleScript下面将会可以添加mole_up和mole_down音效。为每一个MoleUnit重复上述步骤。

  8. tk2dCamera应该已经带有Listener,没有Listener,我们将无法听到任何声音。如果您使用的是旧版本的tk2dcamera没有附带Listener,或正在使用Unity相机,需要添加Listener:在Hierarchy窗口中,选中tk2dcamera对象,然后在Inspector窗口点击AddComponent按钮,然后选择Audio > Audio Listener,Listener将被添加到相机,这就是所有你需要完成的事。
    img/add_listener.png

现在我们已经做完开发一个游戏的所有工作了。好吧,虽然它不是Halo 4这样的游戏,但我们已经覆盖了创建游戏的所有要点:添加一个相机、添加精灵、添加文本、添加声音和脚本。如果你没有游戏美工和游戏音乐师,不用担心,网上有很多的资源你可以下载下来用到后续的游戏制作中。


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(十):播放精灵动画


之前我们已经创建好了动画剪辑,我们可将动画精灵添加到场景了。

  1. 在Hierarchy窗口,点击Create > tk2d > Sprite With Animator。

  2. 在Inspector窗口中,为精灵动画设置一个名字:BigDust。

  3. 在Tk 2d Sprite组件中将Collection设置为Dust,将Sprite设置为Animated_Dust_01。

  4. 在Tk 2d Sprite Animator组件中,将Anim Lib值设置为刚才创建的DustAnim。因为我们只想在地鼠被敲时播放动画,所以不选中Play automatically复选框,选中会让动画在游戏开始时立即播放。动画的位置在这里并不重要,我们会在地鼠被敲时动态的改变它的位置。


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(十一):游戏脚本


现在是时候来完成游戏的所有功能了,尽管我们已经控制了每一个地鼠,但我们仍然需要一些东西来控制整个游戏。

  1. 在Hierarchy窗口,点击Create > tk2d > Empty Game Object并命名为GameScript。

  2. 在Project窗口中,选中MainGameScript并拖拽到Hierarchy窗口的GameScript对象上。

  3. 这个脚本包含游戏摄像机的公共变量,以及一个动画。选中并拖拽摄像机到Game Cam字段中,同样拖拽BigDust到Dust Animator字段中。

如果你想了解游戏脚本是如何工作的,点击这里

现在运行游戏,你就可以看到地鼠从洞里钻出,而你也可以敲他们了。被敲到的地鼠的精灵图片会改变,同时dust animation动画被播放。


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(九):创建精灵动画


到目前为止,除了Dust精灵集合,我们已经使用精灵创建了很多精灵集合。这些精灵与我们打地鼠时一个接一个显示的不同,它是一个动画。

  1. 在Project窗口,点击Create > Folder创建文件夹并命名为Animations。

  2. 选中Animations文件夹,点击Create > tk2d > Sprite Animation并命名为DustAnim。

  3. 选中DustAnim对象,点击Inspector窗口中的Open Editor按钮。

  4. 在新打开的Open Editor窗口中,点击Create > Clip创建动画剪辑。

  5. 将刚创建的动画剪辑命名为DustCloud,在Collection字段选择精灵集合Dust,然后设置Sprite字段为Animated_Dust_01。

  6. 点击Autofill 1..9按钮,将会自动添加后续编号的剪辑到动画剪辑中。

  7. 之后再Wrap Mode字段,选择Once。这个设置的意思是每次动画被触发时,动画只播放一遍然后停止。

  8. 最后,点击Play按钮来对动画进行预览。你可通过修改frame和time设置来对动画播放的快慢进行调整。


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(八):Prefabs 预设体


我们已经有了Mole游戏对象以及脚本来控制它移动,为了让后续更快速及简单的重复使用,我们将它制成Prefab预设体,本质上它是一个模板,我们使用它来制作副本。

  1. 在Project窗口,创建一个名为Prefabs的文件夹。

  2. 在Hierarchy窗口,选中MoleUnit对象并拖拽到Prefabs文件夹。Hierarchy窗口中的MoleUnit、Hole、Dirt和Mole对象会变成蓝色,这是因为这些对象已经是Prefab预设体了。

  3. 你可以从Project窗口拖拽MoleUnit到Hierarchy窗口来将它添加到游戏中。

  4. 你可以根据自己的需要添加不同数量的Hole和Mole游戏对象,记得修改每一个MoleUnit对象的X和Y坐标,直到你向下面一样添加完成:


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(六):添加地鼠


我们已经有了背景,现在需要将地鼠加入游戏中。

每一个“地鼠”包括三个部分:地鼠、洞穴和洞旁边的泥。我们将这三种组合成一个单元Unit。

  1. 首先创建一个洞穴,在Hierarchy窗口中点击 Create > tk2d > Sprite ,在Mole精灵集合中选择Mole_Hole精灵,并命名为Hole。调整Scale及Position让他显示在土地中间。目前摄像机设置为1024*768分辨率,所以将X和Y值分别修改为512和260。记住修改Z值让洞显示在背景图片之前。

  2. 之后,确保在Hierarchy窗口中已经选中了Hole对象,点击Create > tk2d > Sprite,在Mole精灵集合中选择Mole_Hole_Mud精灵,调整Scale、Position并进行命名,让它显示在洞穴之前。这一步中,我们将泥创建为了Hole的子对象,所以所有的Transform值都是相对于洞穴的,它显示在洞穴的中间而不是游戏屏幕的左下角(决定于Sprite的锚点)。而Z值也同样相对于洞穴,调整Z值,让它显示在洞穴前面。

  3. 现在到了地鼠本身,因为地鼠会从洞里钻进钻出,所以我们遇到了一个问题:当地鼠钻到洞里后,你仍然可以在屏幕上的洞下面看到它, 因此为了实现地鼠消失在洞里,我们将使用裁剪后的精灵。在Hierarchy窗口选中洞穴对象,点击Create > tk2d > Clipped Sprite,然后从Mole精灵集合中选择Mole_Normal精灵。

    调整地鼠的Position,让它显示在洞外面,可以将Y值设置为60。然后调整Z值让它显示洞前面、泥后面。同样,你需要给鼹鼠添加一个碰撞盒子,我们使用碰撞盒子来检测地鼠是否被打中。完成后应该如下图一样:

如果你点击了Unity中的Play按钮,游戏会运行,但不会显示任何东西。我们需要添加一些脚本到地鼠对象中来让他完成一些事情,之后我们会添加更多的地鼠。

如果你想将你的游戏工程与我们的进行对比,你可以从这里下载


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(七):脚本


在Hierarchy窗口点击Create > tk2d > Empty GameObject,并重命名这个新对象为MoleUnit。在Inspector窗口修改Transform Position X/Y/Z值。在Hierarchy窗口,拖拽MoleHole到MoleUnit游戏对象,地鼠和泥游戏对象会自动跟随。

提示:如果你修改X和Y并不容易,这里有另外一种方法:

在Hierarchy窗口中拖拽这个空游戏对象到MoleHole对象上,空游戏对象的X和Y值将会改变。

  • 选中空游戏对象,并将Inspector窗口中的X和Y值修改为0。

  • 在Hierarchy窗口中,选中并拖拽空游戏对象到MoleHole上,它将会和MoleHole拥有同样的X和Y值。

  • 选中并拖拽MoleHole对象到空游戏对象中。MoleHole将会成为空游戏对象的一个子对象,并且Hole的X和Y值会变成0。

在Project窗口中,新建文件夹并命名为Scripts。选中Scripts文件夹,创建C#,并命名为MoleScript。

编辑脚本文件,并将代码替换为下面文件中代码:MoleScript C# version

当然,你也可以使用Javascript(UnityScript)代码。Javascript代码使用Create > Javascript来创建。Javascript版本的代码可以从这里找到。tk2d的代码是使用C#编写的,但同样可是使用你编写的任何Javascript文件,在Unity菜单栏中,选择2DToolkit > SetupForJs。

新建一个名为MainGameScript的C#脚本,之后编辑脚本并将MainGameScript这里的代码复制进去。我们会在下一节中看到脚本的更多信息,但现在我们只需要编译MoleScript。

你可以在这里找到MainGameScript的Javascript(UnityScript)版本

当保存完所有的脚本之后,在Project窗口选中MoleScript并拖拽到Hierarchy窗口的MoleUnit对象上,脚本会被添加到游戏对象中。

由于tk2dClippedSprite中包含一个名为Sprite的公共变量,所以我们可以把这个脚本和MoleScript脚本关联起来。在Hierarchy窗口中,选中MoleUnit游戏对象并将它拖拽到Mole对象的MoleScript脚本中的Sprite字段中。

想了解MoleScript是如何工作的,点击这里

如果你想将你的游戏工程与我们的进行对比,你可以从这里下载


本系列教程的所有链接:


2DToolkit官方文档中文版打地鼠教程(五):Static Sprite Batcher 静态精灵批处理


  1. 在Hierarchy窗口点击 Create > tk2d > Static Sprite Batcher,将名称改为Background。

  2. 在Hierarchy窗口,拖拽每一个之前创建的背景对象到你刚添加的Static Sprite Batcher里。

  3. 将所有对象添加到Static Sprite Batcher后,选中它,之后在Inspector中点击Commit按钮提交修改。

    img/batcher_inspector_window.png

  4. Hierarchy窗口中的所有背景精灵会消失,只留下Background这个Static Sprite Batcher。

如果之后你需要修改Static Sprite Batcher中的精灵,你可以在Inspector窗口中单击Edit按钮。所有单独的精灵会再次在Hierarchy窗口中出现,然后你就可以对其进行修改。


本系列教程的所有链接: