用cocos2d 2.1制作一个过河小游戏(2): 牧师与魔鬼Sprite设计

接着上一篇,这次先来讲讲游戏中牧师与魔鬼的设计思路。首先让我们猜想一下在游戏中人物会有怎样的动作?没错,人物应该可以站在岸上,也可以坐在船上。当我点击一个人的时候,如果他在岸上,他就可以跳到船上(前提是船还没有满人);反之亦然。所以概括地说,每次点击一个人物,他可以响应我的点击事件,并且根据现在他所在的位置和环境条件进行移动。

人物的基本方法确定好后,再来考虑一下牧师和魔鬼之间有没有什么不同?其实并没有很大的不同。两种CCSprite加载的图片是不一样的,还有就是当牧师和魔鬼站在岸上的时候,人物的位置也是不一样的(仔细观察前一篇的效果图中,大家可以发现三个牧师和三个魔鬼都是分别对应有固定的位置的)。所以每个人物应当有一个自己的编号,来决定站在岸上时站的位置。

大概分析完后就开始开工吧。首先新建一个cocos2d工程。我使用的是2.1版本,所以一上来系统自动创建的文件还是很多的。


我是第一次用cocos2d来做项目,相信也有一些人可能和我一样是新手吧,上来就被这么多文件吓尿了。其实框架是很简单的:AppDelegate和以往Cocoa项目中的AppDelegate并没有太大区别,只是里面多了许多Cocos2d项目在初始化时需要做的一些工作,例如对平台、屏幕方向大小的确认等。这些暂时可以不必理会,因为我们要做的游戏是在iphone平台上的,屏幕是横向的,和默认设置一致。IntroLayer应该是整个Cocos2d项目的第一个CCScene里面的CCLayer,其作用就是弄个游戏刚进去时的加载界面给你瞅瞅。然后在IntroLayer.m文件的最后我们能看到这样一个函数:


onEnter是进入这个Layer时调用的函数,大意是说这个Layer出现后1秒钟就开始调用HelloWorldLayer.m中的scene方法初始化一个HelloWorldLayer里面的Layer。(关于CCDirector之类的概念如果不清楚可以上网先查查cocos2d工程中CCNode、CCDirector、CCLayer、CCScene等概念的区别和联系)


说白了,HelloWorldLayer就是我们游戏的第一个有效的Layer......


不过在我们工程里还是给它改个名字好,我改成了BaseLayer,里面完成了游戏的逻辑判断部分,是整个游戏设计的关键和中心。具体实现会在后面的帖子里面讲到。根据我们之前的分析,我们为工程添加如下几个类:


1. PersonSprite:人物类,继承自CCSprite,是牧师和魔鬼类的父类,并定义其中的成员变量和方法;

2. PriestSprite:牧师类,继承自PersonSprite;

3. DevilSprite:魔鬼类,继承自PersonSprite;

4. PublicArg:公有变量类,储存整个游戏工程用到的公有变量,设计为单例模式。


1. PersonSprite

 PersonSprite.h

#import "CCSprite.h"
#import "PublicArg.h"

@class PersonSprite;

@protocol MoveSpriteDelegate <NSObject>

@required
-(void)moveSprite:(PersonSprite *)sprite;

@end

@interface PersonSprite : CCSprite <CCTouchOneByOneDelegate>

@property (assign,nonatomic) Side personSide;  //record the side the person is on
@property (strong,nonatomic) id<MoveSpriteDelegate> delegate;
@property (assign,nonatomic) int number;        //the number of the person

@end

先来看看人物类的头文件。上面首先定义了一个MoveSpriteDelegate协议,下面还有一个实现该协议的id成员变量delegate。想必大家都知道这里要用到的是委托吧。没错, CCSprite并没有触屏接收的机制,所以我们需要在CCSprite中自己实现CCSprite的触屏实现。而由于整个游戏中BaseLayer要负责各个精灵的挪动,所以PersonSprite收到触屏请求后需要委托给BaseLayer来完成自己的移动行为。

除了delegate外,还有两个变量personSide和number。personSide表示当前人物是在哪边。Side类型是一个自定义枚举类型,定义如下:

typedef enum
{
    Left = 100,Right = 101,BOAT = 102
} Side;

一个人物可能在左岸、右岸或者船上。

number则是人物编号,关系到人物在岸上时站的位置。下面来看看实现文件。


PersonSprite.m

#import "PersonSprite.h"

@implementation PersonSprite

-(void)onEnter
{
    //register touch event
    [[[CCDirector sharedDirector] touchDispatcher]addTargetedDelegate:self priority:1 swallowsTouches:YES];
    [super onEnter];
}

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    return [self isContainsTouchPoint:touch];
}

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
    AppController *delegate = [[UIApplication sharedApplication]delegate];
    
    if (delegate.gameState != InGame)
        return;
    
    [_delegate moveSprite:self];
}

-(void)onExit
{
    [[[CCDirector sharedDirector] touchDispatcher]removeDelegate:self];
}

//get the rect of current sprite
-(CGRect)spriteRect
{
    //culculate the rect of the sprite
    return CGRectMake( _position.x - _contentSize.width*_anchorPoint.x,
                      _position.y - _contentSize.height*_anchorPoint.y,
                      _contentSize.width, _contentSize.height);
}

//detect whether the sprite is touched
-(BOOL)isContainsTouchPoint:(UITouch*)touch
{
    CGRect spriteRect = [self spriteRect];
    spriteRect.origin = CGPointZero;
    CGPoint touchPointInView = [touch locationInView:[touch view]];
    
    //convert to openGL coordinate
    touchPointInView = [[CCDirector sharedDirector] convertToGL:touchPointInView];
    CGPoint touchPoint = [self convertToNodeSpace:touchPointInView];
    
    return CGRectContainsPoint(spriteRect, touchPoint);
}

@end

在OnEnter和OnExit方法中,我们分别注册和取消CCSprite接收触屏事件的功能。PersonSprite遵守CCTouchOneByOneDelegate, 所以可以实现ccTouchBegan和ccTouchEnded设置手指触屏下去的瞬间和松开的时候做的事情。注意ccTouchBegan需要返回一个布尔值,这个函数不可或缺。spriteRect函数和 isContainsTouchPoint 函数分别计算PersonSprite在屏幕中的大小以及判断触点是否在PersonSprite的有效区域内。总的来说设置并不困难。

值得一提的是,在ccTouchEnded方法中,我们看到一个InGame值。这也是一个枚举,定义了游戏当前运行的阶段,枚举定义如下:

typedef enum
{
    BeforeGame,     //state: before game start
    InGame,         //state: game running
    BlockGame,      //state: boat is moving and nothing can be done
    PauseGame,      //state: game pause
    EndGame         //state: game end
} GameState;

我是把GameState和之前的Side枚举都放在了AppDelegate中。设置游戏状态是很重要的:如在暂停的时候玩家不能点击人物、船在移动的时候不能点击人物、游戏结束的时候释放资源,等等。这些都要通过游戏状态来判断。在这里,很简单地,我们认为只有在InGame状态时,玩家点击精灵才是有效的。


2. PriestSprite

PriestSprite.h

#import "PersonSprite.h"

@interface PriestSprite : PersonSprite

+(id)initPriestWithFile:(NSString *)file withNumber:(int)number;

@end

PriestSprite.m

#import "PriestSprite.h"

@implementation PriestSprite

+(id)initPriestWithFile:(NSString *)file withNumber:(int)number
{
    PriestSprite * temp = [PriestSprite spriteWithFile:file];
    
    temp.number = number;
    temp.personSide = Right;
    [temp setScale:0.5f];
    
    PublicArg *arg = [PublicArg sharedArg];
    
    [temp setPosition:ccp(arg.priest_begin_right.x + number * arg.person_interval, arg.priest_begin_right.y)];
    
    return  temp;
}

@end

由于在PersonSprite中我们已经完成了许多事情,所以子类的实现就比较简单了。initPriestWithFile:withNumber: 是我们自己定义的一个初始化函数,用来初始化Priest的各个变量。值得关注的是,代码中Priest根据自己的number情况已经把自己放到合适的位置上去了。 arg.priest_begin_right、 arg.person_interval、 arg.priest_begin_right等都是PublicArg中定义的常量,待会可以看到。


3. DevilSprite

DevilSprite.h

#import "PersonSprite.h"

@interface DevilSprite : PersonSprite

+(id)initDevilWithFile:(NSString *)file withNumber:(int)number;

@end

DevilSprite.m

@implementation DevilSprite

+(id)initDevilWithFile:(NSString *)file withNumber:(int)number
{
    DevilSprite * temp = [DevilSprite spriteWithFile:file];
    
    temp.number = number;
    temp.personSide = Right;
    [temp setScale:0.5f];
    
    PublicArg *arg = [PublicArg sharedArg];
    
    [temp setPosition:ccp(arg.devil_begin_right.x + number * arg.person_interval, arg.devil_begin_right.y)];
    
    return  temp;
}


@end

Devil和Priest实现是类似的,故不再多说。


4. PublicArg

PublicArg.h

#import <Foundation/Foundation.h>
#import "AppDelegate.h"

@interface PublicArg : NSObject

@property CGPoint boatRightPoint;       //central location of boat when on the right
@property CGPoint boatLeftPoint;       //central location of boat when on the left

@property CGPoint priest_begin_right;   //left-most priest's location on right bank
@property CGPoint devil_begin_right;   //left-most devil's location on right bank
@property CGPoint priest_begin_left;   //right-most priest's location on right bank
@property CGPoint devil_begin_left;   //right-most devil's location on right bank
@property int person_interval;         //interval between 2 persons

+(PublicArg *)sharedArg;

@end

PublicArg.m

#import "PublicArg.h"

@implementation PublicArg

static PublicArg *arg = nil;

+(PublicArg *)sharedArg
{
    @synchronized(self)
    {
        if (arg == nil)
        {
            arg = [[super allocWithZone:NULL]init];
        }
    }
    return arg;
}

- (id)init
{
    if (self = [super init]) {
        AppController *delegate = [[UIApplication sharedApplication]delegate];
        CGSize screenSize = delegate.screenSize;
        
        _boatRightPoint = ccp(screenSize.width/2+80, screenSize.height/2-105);
        _boatLeftPoint = ccp(screenSize.width/2-80, screenSize.height/2-105);
        
        _priest_begin_right = ccp(screenSize.width/2 + 140,screenSize.height/2-40);
        _devil_begin_right = ccp(screenSize.width/2 + 220,screenSize.height/2-40);
        _priest_begin_left = ccp(screenSize.width/2 - 180,screenSize.height/2-45);
        _devil_begin_left = ccp(screenSize.width/2 - 260,screenSize.height/2-45);
        _person_interval = 25;
    }
    return self;
}


+ (id)allocWithZone:(NSZone*)zone {
    return [self sharedArg];
}

- (id)copyWithZone:(NSZone *)zone {
    return self;
}

@end

PublicArg中定义了各种游戏常量:牧师和魔鬼站在左右边的时候最靠边的牧师、魔鬼的水平位置和竖直位置;船在两边停靠的坐标;两个站在岸上的人之间的距离等等。在.m文件中对这些量进行初始化。而且类的实现使用了单例模式,只有在第一次调用shareArg的时候对变量初始化。


大概先是这样。下次将会讲解一下船BoatSprite和两岸BankSprite的设计与实现。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值