用cocos2d 2.1制作一个过河小游戏(4): 游戏主逻辑BaseLayer设计
前段时间一直在忙。没有时间更新博客。今天还是抽点时间把最后一小部分游戏的实现放上来吧。
BaseLayer.h:
#import <GameKit/GameKit.h> #import "cocos2d.h" #import "AppDelegate.h" #import "PersonSprite.h" #import "PriestSprite.h" #import "DevilSprite.h" #import "BoatSprite.h" #import "BankSprite.h" @interface BaseLayer : CCLayer <MoveSpriteDelegate> @property (strong,nonatomic) BankSprite* rightBank; @property (strong,nonatomic) BankSprite* leftBank; @property (strong,nonatomic) BoatSprite* boat; @property (assign,nonatomic) int time; //set the timer +(CCScene *) scene; @end
记得在(2)讲PersonSprite的时候讲过,我们须要BaseLayer来取代PersonSprite来移动图层上的精灵。
因此我们在BaseLayer头文件定义的时候就须要遵守MoveSpriteDelegate协议了。(忘记了的能够翻回文章(2)看一下详细细节)看看BaseLayer的类成员。一共同拥有两个河岸和一条船,另一个整数记录剩余时间。因为BaseLayer并不须要开放变量给其它类使用,所以这些河岸对象和船的定义放在头文件中还是实现文件中的匿名类别区域里面关系都不大。我把这几个类成员放在这里是想将整个游戏的框架表示得清晰一些。
严格地来说,放在.m文件的扩展类别区域更为安全。
有人可能会问:“为什么不把6个过河的人也定义在头文件中呢?”事实上我觉得这样做也是能够的,仅仅是由于考虑到BaseLayer对人和船、河岸的处理是不一样的:对于人来说,BaseLayer是通过代理的形式操作人的。即当玩家点击某个人的时候。事件从该人相应的PersonSprite对象開始传递,而PersonSprite通过delegate的形式交由BaseLayer来实现。在delegate參数传递的过程中,BaseLayer是知道哪个人产生了事件的(能够类比一下UIButton。当点击button产生事件的时候,Controller接收到事件时是能够知道由哪个button被按下导致触发了事件的)。故对于BaseLayer来说。须要使用到PersonSprite的情况仅仅有:1. 对PersonSprite的初始化。 2. 调用托付函数moveSprite。
所以并不须要把PersonSprite也作为成员变量;而对于船来说。由于其是由BaseLayer直接操纵的(控制船的移动),故在BaseLayer中会有多个函数使用到BoatSprite的对象。因此将BoatSprite设置为BaseLayer的成员变量更为方便。BankSprite同理。
来到.m文件,首先看一下静态变量以及扩展类别的声明:
#import "BaseLayer.h" #import "PublicArg.h" #import "SimpleAudioEngine.h" #include "AI.h" static NSString *PRIEST_IMAGE_NAME = @"priest.png"; static NSString *DEVIL_IMAGE_NAME = @"devil.png"; static NSString *BOAT_IMAGE_NAME = @"boat.png"; static NSString *BACKGROUND_IMAGE_NAME = @"background.jpg"; static NSString *WATER_IMAGE_NAME = @"water.png"; static NSString *BG_MUSIC = @"background.mp3"; static NSString *CLICK_EFF = @"click.mp3"; static NSString *FAIL_EFF = @"fail.mp3"; static NSString *PERSON_EFF = @"person.mp3"; static NSString *WATER_EFF = @"water.wav"; static NSString *WIN_EFF = @"win.mp3"; static int timeLimit = 200; //set the time limit @interface BaseLayer() @property (strong,nonatomic) AppController *delegate; //list the items here because the title would be changed @property CCMenuItem *beginItem; @property CCMenuItem *helpItem; @property CCMenuItem *pauseItem; @property CCMenuItem *resumeItem; @property CCMenuItem *endItem; @property CCMenuItem *transitItem; @property CCLabelTTF *timeShow; @property CCMenu *goMenu; @property CCMenu *systemMenu; @property CCSprite *water; @end
静态变量中设置的都是图片的地址以及倒计时的总时长。而类别扩展中我定义了一些button成员变量。这些就是游戏中的菜单。
因为点击不同选项的时候有可能造成其它选项的转台变化(如点击暂停的时候。helpbutton应该是不可用的)。所以我将这些button都放到成员变量中统一管理。注意到最后另一个water的sprite对象,这个是画面中在船上面的水图层,用来遮挡船的下沿,让效果更逼真一些。
看看几个初始化函数:
+(CCScene *) scene { CCScene *scene = [CCScene node]; BaseLayer *layer = [BaseLayer node]; [scene addChild: layer]; return scene; } //initialize instance -(id) init { if( (self=[super init]) ) { //initiate the game state _delegate = [[UIApplication sharedApplication] delegate]; _delegate.gameState = BeforeGame; _delegate.screenSize = [[CCDirector sharedDirector] winSize]; //initiate the public arguments [PublicArg sharedArg]; //set the background image which is also a sprite CCSprite *background = [CCSprite spriteWithFile:BACKGROUND_IMAGE_NAME]; [background setScale:0.5f]; [background setPosition:ccp(_delegate.screenSize.width/2, _delegate.screenSize.height/2)]; [self addChild:background]; [self initMenu]; //preload the background music & effects //[[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:BG_MUSIC]; //[[SimpleAudioEngine sharedEngine] preloadEffect:CLICK_EFF]; //[[SimpleAudioEngine sharedEngine] preloadEffect:FAIL_EFF]; //[[SimpleAudioEngine sharedEngine] preloadEffect:PERSON_EFF]; //[[SimpleAudioEngine sharedEngine] preloadEffect:WATER_EFF]; //[[SimpleAudioEngine sharedEngine] preloadEffect:WIN_EFF]; } return self; } -(void)initSprites { //set the 2 banks & 6 persons _rightBank = [[BankSprite alloc] initWithSide:Right]; //start place _leftBank = [[BankSprite alloc]initWithSide:Left]; //end place _rightBank.tag = 101; _leftBank.tag = 100; [self addChild:_rightBank]; [self addChild:_leftBank]; for(int i = 0;i<3;++i) { PriestSprite *temp_priest = [PriestSprite initPriestWithFile:PRIEST_IMAGE_NAME withNumber:i]; DevilSprite *temp_devil = [DevilSprite initDevilWithFile:DEVIL_IMAGE_NAME withNumber:i]; temp_priest.delegate = self; temp_devil.delegate = self; temp_priest.tag = i; temp_devil.tag = i + 3; [self addChild:temp_priest]; [self addChild:temp_devil]; } //set the boat _boat = [BoatSprite initBoatWithFile:BOAT_IMAGE_NAME]; [self addChild:_boat]; //set the water _water = [CCSprite spriteWithFile:WATER_IMAGE_NAME]; [_water setScale:0.5]; [_water setPosition:ccp(_delegate.screenSize.width/2-40, _delegate.screenSize.height/2)]; [self addChild:_water]; } -(void)initMenu { //set the menu list [CCMenuItemFont setFontSize:28]; _beginItem = [CCMenuItemFont itemWithString:@"Start" target:self selector:@selector(beginGame)]; _helpItem = [CCMenuItemFont itemWithString:@"Help" target:self selector:@selector(help)]; _pauseItem = [CCMenuItemFont itemWithString:@"Pause" target:self selector:@selector(pauseGame)]; _resumeItem = [CCMenuItemFont itemWithString:@"Resume" target:self selector:@selector(resumeGame)]; _endItem = [CCMenuItemFont itemWithString:@"End" target:self selector:@selector(endGame)]; _systemMenu = [CCMenu menuWithItems:_beginItem, _pauseItem, _resumeItem, _endItem, _helpItem, nil]; [_systemMenu alignItemsHorizontallyWithPadding:45.0]; [_systemMenu setPosition:ccp( _delegate.screenSize.width/2 , _delegate.screenSize.height/2 + 100)]; [self addChild:_systemMenu]; _transitItem = [CCMenuItemFont itemWithString:@"Go" target:self selector:@selector(transit)]; [_transitItem setColor:ccc3(0, 0, 0)]; [_transitItem setVisible:NO]; _goMenu = [CCMenu menuWithItems:_transitItem, nil]; [_goMenu setPosition:ccp( _delegate.screenSize.width/2 , _delegate.screenSize.height/2 +10)]; [self addChild:_goMenu]; //set the items [_resumeItem setIsEnabled:NO]; [_endItem setIsEnabled:NO]; [_pauseItem setIsEnabled:NO]; [_helpItem setIsEnabled:NO]; //set the time shower _timeShow = [[CCLabelTTF alloc] init]; [_timeShow setFontName:@"Marker Felt"]; [_timeShow setPosition:ccp(_delegate.screenSize.width/2, _delegate.screenSize.height/2 + 50)]; [_timeShow setFontSize:24.0f]; [self addChild:_timeShow]; }
顺带打开AppDelegate.h:
#import <UIKit/UIKit.h> #import "cocos2d.h" typedef enum { Left = 100,Right = 101,BOAT = 102 } Side; //list out the game state typedef enum { BeforeGame, //state: before game start InGame, //state: game running BlockGame, //state: boat is moving the nothing can be done PauseGame, //state: game pause EndGame //state: game end } GameState; // Added only for iOS 6 support @interface MyNavigationController : UINavigationController <CCDirectorDelegate> @end @interface AppController : NSObject <UIApplicationDelegate> { UIWindow *window_; MyNavigationController *navController_; CCDirectorIOS *__unsafe_unretained director_; // weak ref } @property (nonatomic, strong) UIWindow *window; @property (readonly) MyNavigationController *navController; @property (unsafe_unretained, readonly) CCDirectorIOS *director; @property (assign,nonatomic) CGSize screenSize; //the screen size @property (assign,nonatomic) GameState gameState; //game state @end
Scene函数是BaseLayer的初始化函数:首先建立一个新的场景。然后向场景中增加BaseLayer布景。大多数Layer都有Scene这个函数。这个是由系统自己主动生成的。在init函数中,初始化了delegate变量。delegate是AppDelegate的单例。
在AppDelegate的头文件里,我们能够知道它记录了游戏的状态和屏幕大小。由于这些是与游戏总体有关的变量,所以放在了全局的AppDelegate中,并在BaseLayer中使用delegate对其进行引用。
之后创建了一个background对象并增加到布景中。同一时候初始化菜单以及计时器。
在initSprite方法中。我们初始化了6个人而且分别以tag值0到5记录他们。这是为后面将其放在岸上的指定位置做准备的。
接下来看看代理方法moveSprite的实现:
#pragma mark MoveSprite Delegate -(void)moveSprite:(PersonSprite *)sprite { if (_delegate.gameState != InGame) return; //[[SimpleAudioEngine sharedEngine]playEffect:PERSON_EFF]; PublicArg *arg = [PublicArg sharedArg]; switch (sprite.personSide) { case Left: case Right: //person is on the left { //when person and boat are at different side if ( (sprite.personSide == Left && _boat.boatSide == Right) || (sprite.personSide == Right && _boat.boatSide == Left)) return; //go on the boat switch (_boat.priestNumber + _boat.devilNumber ) { case 0: { [sprite setPosition:ccp(_boat.position.x-20, _boat.position.y+25)]; [_boat.personList addObject:sprite]; break; } case 1: { [_boat.personList addObject:sprite]; PersonSprite *temp = [_boat.personList objectAtIndex:0]; [sprite setPosition:ccp(2 * _boat.position.x - temp.position.x, _boat.position.y+25)]; break; } case 2: return; } if ([sprite isKindOfClass:[PriestSprite class]]) { _boat.priestNumber++; BankSprite * temp_bamk = (BankSprite *)[self getChildByTag:sprite.personSide]; temp_bamk.priestNumber --; } else if ([sprite isKindOfClass:[DevilSprite class]]) { _boat.devilNumber++; BankSprite * temp_bamk = (BankSprite *)[self getChildByTag:sprite.personSide]; temp_bamk.devilNumber --; } [sprite setPersonSide:BOAT]; break; } case BOAT: //person is on the boat { if ([sprite isKindOfClass:[PriestSprite class]]) { if (_boat.boatSide == Left) { [sprite setPosition:ccp(arg.priest_begin_left.x + sprite.number * arg.person_interval, arg.priest_begin_left.y )]; [sprite setPersonSide:Left]; _leftBank.priestNumber ++; } else if (_boat.boatSide == Right) { [sprite setPosition:ccp(arg.priest_begin_right.x + sprite.number * arg.person_interval, arg.priest_begin_right.y )]; [sprite setPersonSide:Right]; _rightBank.priestNumber ++; } _boat.priestNumber--; } else if ([sprite isKindOfClass:[DevilSprite class]]) { if (_boat.boatSide == Left) { [sprite setPosition:ccp(arg.devil_begin_left.x + sprite.number * arg.person_interval, arg.devil_begin_left.y )]; [sprite setPersonSide:Left]; _leftBank.devilNumber++; } else if (_boat.boatSide == Right) { [sprite setPosition:ccp(arg.devil_begin_right.x + sprite.number * arg.person_interval, arg.devil_begin_right.y )]; [sprite setPersonSide:Right]; _rightBank.devilNumber ++; } _boat.devilNumber--; } [_boat.personList removeObject:sprite]; if([self judgeWin]) { [[SimpleAudioEngine sharedEngine] stopBackgroundMusic]; [[SimpleAudioEngine sharedEngine]playEffect:WIN_EFF]; [_timeShow setString:@"Win!"]; [_timeShow setColor:ccc3(255.0, 215.0, 0.0)]; _delegate.gameState = EndGame; [self clearItem]; } break; } } //set the go button according to the number of persons on the boat if (_boat.priestNumber + _boat.devilNumber > 0) [_transitItem setIsEnabled:YES]; else [_transitItem setIsEnabled:NO]; }
当游戏不是在“游戏中”状态时(InGame)。点击精灵应当不会响应,这个非常easy理解。最重要的是理解一下moveSprite究竟会面对哪些情况,以及在每种情况下做了什么事情:当玩家点击一个牧师/魔鬼的时候,这人有可能在船上。也有可能在岸上。于是我们分下面情况讨论:
1. 当人在岸上时。上船的时候要推断船上有没有人,来决定人在船上的位置。
(避免在船上出现人物重叠的情况)
2. 当人在船上时,查看船在哪个岸边,然后将人物放上去就可以。
接下来是船过河的函数,比較简单。仅仅需改变船的side属性,然后对船和船上的人都设置移动就可以。
-(void)transit { if (_boat.priestNumber + _boat.devilNumber == 0) return; [[SimpleAudioEngine sharedEngine]playEffect:WATER_EFF]; _delegate.gameState = BlockGame; // sprites can not be moved [_transitItem setIsEnabled:NO]; [_helpItem setIsEnabled:NO]; int moveDis = 0; if (_boat.boatSide == Right) { moveDis = -160; _boat.boatSide = Left; } else if (_boat.boatSide == Left) { moveDis = 160; _boat.boatSide = Right; } CCMoveBy *move = [CCMoveBy actionWithDuration:1.0 position:ccp(moveDis, 0)]; id clear = [CCCallFunc actionWithTarget:self selector:@selector(clear)]; [_boat runAction:[CCSequence actions:move,clear, nil]]; //move the persons on the boat for(int i = 0;i< _boat.priestNumber + _boat.devilNumber ;++i) { CCMoveBy *myMove =[CCMoveBy actionWithDuration:1.0 position:ccp(moveDis, 0)]; PersonSprite *sprite = [_boat.personList objectAtIndex:i]; [sprite runAction:myMove]; } }
推断成功与失败的函数:
-(BOOL)judgeWin { if (_leftBank.priestNumber == 3 && _leftBank.devilNumber == 3) return true; return false; } -(BOOL)judgeLose { int leftPriestNumber = _leftBank.priestNumber + abs(_boat.boatSide - Right) * _boat.priestNumber; int rightPriestNumber = _rightBank.priestNumber + abs(_boat.boatSide - Left) * _boat.priestNumber; int leftDevilNumber = _leftBank.devilNumber + abs(_boat.boatSide - Right) * _boat.devilNumber; int rightDevilNumber = _rightBank.devilNumber + abs(_boat.boatSide - Left) * _boat.devilNumber; if ( (leftDevilNumber > leftPriestNumber && leftPriestNumber > 0 ) || (rightDevilNumber > rightPriestNumber && rightPriestNumber >0)) return true; }
用于游戏状态转换的函数(開始、暂停、结束、继续等)
-(void)beginGame { //play background music [[SimpleAudioEngine sharedEngine] playBackgroundMusic:BG_MUSIC loop:YES]; [[SimpleAudioEngine sharedEngine]playEffect:CLICK_EFF]; _delegate.gameState = InGame; [self initTimer:timeLimit]; [self schedule:@selector(updateTimeDisplay) interval:1]; [_beginItem setIsEnabled:NO]; [_pauseItem setIsEnabled:YES]; [_endItem setIsEnabled:YES]; [_helpItem setIsEnabled:YES]; [_transitItem setVisible:YES]; [_transitItem setIsEnabled:NO]; [_timeShow setColor:ccc3(255.0f, 255.0f, 255.0f)]; NSString *time = [NSString stringWithFormat:@"%.2d:%.2d", _time / 60, _time % 60]; [_timeShow setString:time]; [self removePreviousSprite]; [self initSprites]; } -(void)pauseGame { [[SimpleAudioEngine sharedEngine] pauseBackgroundMusic]; [[SimpleAudioEngine sharedEngine]playEffect:CLICK_EFF]; _delegate.gameState = PauseGame; [self unschedule:@selector(updateTimeDisplay)]; [_resumeItem setIsEnabled:YES]; [_pauseItem setIsEnabled:NO]; [_helpItem setIsEnabled:NO]; [_transitItem setVisible:NO]; } -(void)resumeGame { [[SimpleAudioEngine sharedEngine] resumeBackgroundMusic]; [[SimpleAudioEngine sharedEngine]playEffect:CLICK_EFF]; _delegate.gameState = InGame; [_resumeItem setIsEnabled:NO]; [_pauseItem setIsEnabled:YES]; [_helpItem setIsEnabled:YES]; [_transitItem setVisible:YES]; [self schedule:@selector(updateTimeDisplay) interval:1]; } -(void)endGame { [[SimpleAudioEngine sharedEngine]stopBackgroundMusic]; [[SimpleAudioEngine sharedEngine]playEffect:CLICK_EFF]; _delegate.gameState = EndGame; [self clearItem]; [self setTime:0]; NSString *time = [NSString stringWithFormat:@"%.2d:%.2d",_time / 60, _time % 60]; [_timeShow setString:time]; [self clearGame]; }
清空屏幕上的sprites。用于一场游戏结束后清空屏幕上元素,避免内存泄露。
-(void)clearItem { [_resumeItem setIsEnabled:NO]; [_endItem setIsEnabled:NO]; [_pauseItem setIsEnabled:NO]; [_beginItem setIsEnabled:YES]; [_helpItem setIsEnabled:NO]; [_transitItem setVisible:NO]; [self unschedule:@selector(updateTimeDisplay)]; } -(void)clear { _delegate.gameState = InGame; // sprites can not be moved [_transitItem setIsEnabled:YES]; [_helpItem setIsEnabled:YES]; if([self judgeLose]) { [[SimpleAudioEngine sharedEngine] stopBackgroundMusic]; [[SimpleAudioEngine sharedEngine]playEffect:FAIL_EFF]; _delegate.gameState = EndGame; [_timeShow setString:@"Lose!"]; [_timeShow setColor:ccc3(255.0, 0.0, 0.0)]; _delegate.gameState = EndGame; [self clearItem]; } } //remove the sprites of last scene -(void)removePreviousSprite { for(int i = 0;i<6;++i) { PersonSprite *temp = (PersonSprite *)[self getChildByTag:i]; if (temp != NULL) [self removeChildByTag:i]; } if (_boat != NULL) [self removeChild:_boat]; if (_rightBank != NULL) [self removeChild:_rightBank]; if (_leftBank != NULL) [self removeChild:_leftBank]; if (_water != NULL) [self removeChild:_water]; } -(void)clearGame { //change to a new scene [[CCDirector sharedDirector]replaceScene:[BaseLayer scene]]; }
其它辅助函数:
-(void)updateTimeDisplay { _time--; NSString *time = [NSString stringWithFormat:@"%.2d:%.2d",_time / 60, _time % 60]; [_timeShow setString:time]; if (_time <= 0) [self endGame]; } -(void)initTimer:(int)interval { _time = interval; }
最后是help函数。help功能主要做的是依据当前游戏中人物的位置,自己主动为玩家走下一步。算是一种比較简单的决策的体现吧。
原理非常easy。就是找出一条能通关的路径,然后把各种合理的游戏状态连接到路径上。即设置一个有限状态机,储存路径图,给定输入和当前状态后,系统便能算出下一个状态。
help函数:
-(void)help { [[SimpleAudioEngine sharedEngine]playEffect:CLICK_EFF]; int leftPriestNumber = _leftBank.priestNumber + abs(_boat.boatSide - Right) * _boat.priestNumber; int rightPriestNumber = _rightBank.priestNumber + abs(_boat.boatSide - Left) * _boat.priestNumber; int leftDevilNumber = _leftBank.devilNumber + abs(_boat.boatSide - Right) * _boat.devilNumber; int rightDevilNumber = _rightBank.devilNumber + abs(_boat.boatSide - Left) * _boat.devilNumber; int boatSide = _boat.boatSide - 100; int deltaPriest = 0, deltaDevil = 0; getHelp(leftPriestNumber,leftDevilNumber,rightPriestNumber,rightDevilNumber,boatSide,deltaPriest,deltaDevil); int tempNum = _boat.priestNumber + _boat.devilNumber; for(int i = 0;i< tempNum ; ++i) [self moveSprite:[_boat.personList objectAtIndex:0]]; int i = 0; while (deltaPriest > 0) { PriestSprite *mySprite = (PriestSprite *)[self getChildByTag:i]; if (mySprite.personSide == _boat.boatSide) { [self moveSprite:mySprite]; deltaPriest --; } i++; } i = 3; while (deltaDevil > 0) { DevilSprite *mySprite = (DevilSprite *)[self getChildByTag:i]; if (mySprite.personSide == _boat.boatSide) { [self moveSprite:mySprite]; deltaDevil --; } i++; } [self transit]; }
这里调用的是一个AI.cpp文件。cpp文件里储存了路径。
AI.h:
#ifndef RiverCrossing_AI_h #define RiverCrossing_AI_h struct state{ int boat_side; int left_pastor; int left_devil; int right_pastor; int right_devil; }; // //void initWays(int * ways); //int hash(state s); //state deHash(int key); //void change(state now,state next, // int & pastor,int & devil); /* input:state boat_side(0=left,1=right) return:void */ void getHelp (int left_pastor,int left_devil, int right_pastor,int right_devil, int boat_side,int& pastor,int &devil); #endif
AI.cpp:
#include "AI.h" #include<iostream> #include<cmath> using namespace std; void initWays(int * ways) { ways[10033]=231; ways[231]=10132; ways[10132]=330; ways[330]=10231; ways[10231]=2211; ways[2211]=11122; ways[11122]=3102; ways[3102]=13003; ways[13003]=3201; ways[3201]=13102; ways[13102]=3300; ways[3300] = 3300; ways[1122]=10132; ways[132]=10033; } int hash(state s) { int key; key=s.boat_side*10000+s.left_pastor*1000+s.left_devil*100+s.right_pastor*10+s.right_devil; return key; } state deHash(int key) { int boat_side=key/10000; int left_pastor=key%10000/1000; int left_devil=key%1000/100; int right_pastor=key%100/10; int right_devil=key%10; struct state s={boat_side,left_pastor,left_devil,right_pastor,right_devil}; return s; } void change(state now,state next, int & pastor,int & devil) { pastor=abs(now.left_pastor-next.left_pastor); devil=abs(now.left_devil-next.left_devil); } /* input:state boat_side(0=left,1=right) return:void */ void getHelp(int left_pastor,int left_devil, int right_pastor,int right_devil, int boat_side, int & pastor,int & devil) { int ways[13334]; initWays(ways); struct state now={boat_side,left_pastor,left_devil,right_pastor,right_devil}; int now_state_key=hash(now); int next_state_key=ways[now_state_key]; struct state next=deHash(next_state_key); change(now,next,pastor,devil); }
特别注意一下。在.m文件里要想编译cpp文件,须要把.m的后缀改为.mm才行。