<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                Learn IPhoneand iPad Cocos2d Game Delevopment》第8章 。 這種類型的游戲(shoot’emup游戲)最重要的是什么?射擊的目標和需要躲避的子彈。本章,將為游戲添加一些敵人以及一個大 boss。 敵人和玩家將使用新的BulletCache 類射擊不同的子彈,這些子彈來自同一個 pool。這個緩沖類會重用無效的子彈,以避免重復的內存分配和釋放動作。同樣,敵人會使用EnemyCache 類,因為待會屏幕上會出現成堆的敵人。 顯然玩家可以向敵人射擊。我會介紹基于組件編程的概念,用一種模板化的方式擴展游戲角色。除了 shooting 組件和 moving 組件,我們還會為 boss 老怪創建 healthbar 組件(生命值,俗稱“血槽”)。畢竟,老怪不應該是一下就能pk 掉的,其生命值總是被一點點減少直至徹底干掉它。 ## 一、添加 BulletCache 類 該類在是 “一站式” 的,可以一次性生成許多子彈。原來這些代碼是放在 GameScene 類中,但這(指生成子彈)顯然不該由 GameScene 來管。下面顯示 BulletCache 的頭文件,它包括了CCSpriteBatchNode 對象和無效子彈計數器nextInactiveBullet: ~~~ #import "cocos2d.h" @interface BulletCache : CCNode { CCSpriteBatchNode* batch; ?int nextInactiveBullet; } -(void) shootBulletAt: (CGPoint)startPositionvelocity:(CGPoint)velocity frameName:(NSString*)frameName; @end ~~~ 為了把 代碼重構到 GameScene類之外,我需要把 initialization 方法和射擊子彈的方法移到 BulletCache 類(代碼見后)。接著,我決定使用一個CCSpriteBatchNode 變量,以免在每次需要這個對象時就得調用一次[CCNode CCSpriteBatchNode]方法。這會帶來細微的性能優化。由于我會在類 GameScene 中加入 BulletCache 對象,因此很容易就能把 sprite batch node 傳給 BulletCache。 注意,新的 BulletCache有一個問題,增加了scene的層次——一個額外的 CCNode。如果你擔心這點,你也可以把 sprite batch node放在GameScene類中,用一個方法從BulletCahce 獲取這個 sprite batch node。 但是,額外的函數調用開銷有可能會使性能得以下降。如果你懷疑是不是真的對性能由影響,那就讓你的代碼可讀性更好些。當有必要進行性能優化的時候再重構你的代碼。 ~~~ #import "BulletCache.h" #import "Bullet.h" @implementation BulletCache -(id) init { if ((self = [super init])) { // 從當前貼圖集中獲得角色幀 CCSpriteFrame* bulletFrame =[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"bullet.png"]; // 使用角色幀的貼圖構建CCSpriteBatchNode batch = [CCSpriteBatchNodebatchNodeWithTexture:bulletFrame.texture]; ?[self addChild:batch]; // 創建子彈并加到 batch for (int i = 0; i < 200; i++) { Bullet* bullet =[Bullet bullet]; bullet.visible =NO; [batchaddChild:bullet]; } return self; }} ?-(void) shootBulletAt:(CGPoint)startPositionvelocity:(CGPoint)velocity frameName:(NSString*)frameName{ CCArray* bullets = [batch children]; CCNode* node = [bullets objectAtIndex:nextInactiveBullet]; NSAssert([node isKindOfClass:[Bulletclass]], @"not a Bullet!"); Bullet* bullet = (Bullet*)node; [bullet shootBulletAt:startPositionvelocity:velocity frameName:frameName]; nextInactiveBullet++; if (nextInactiveBullet >= [bulletscount]) { nextInactiveBullet= 0; } } @end ~~~ shootBulletAt方法已經完全變了。它有3個參數:startPosition,velocity和frameName——取代 Ship類指針。然后這些參數被傳遞給 Bullet 類的 shootBulletAt 方法,這個方法現在已經變為: ~~~ -(void) shootBulletAt:(CGPoint)startPositionvelocity:(CGPoint)vel frameName:(NSString*)frameName { self.velocity = vel; self.position = startPosition; self.visible = YES; // 改變子彈的貼圖,設置一個不同的角色幀去顯示 CCSpriteFrame *frame = [[CCSpriteFrameCachesharedSpriteFrameCache] spriteFrameByName:frameName]; [self setDisplayFrame:frame]; [self scheduleUpdate]; } ~~~ velocity 和position 被直接賦值給 bullet。這意味著調用 shootBulletAt 方法的代碼必需自己決定子彈的位置、方向和速度。這出于這樣的考慮:子彈射擊的動作會適應更多的變化,包括可以改變子彈的角色幀(用setDisplayFrame 方法)。因為子彈使用的是相同的貼圖集、相同的貼圖,所以需要通過設置相應的貼圖幀來改變子彈的顯示。實際上,渲染貼圖的不同部分很輕松,并不會帶來額外的開銷。 在編輯 Bullet 類時,我還修正了一個邊界問題——只有子彈移出屏幕右邊時,才會設為不可見并被放會重用列表(其實這是一個bug)。通過在update方法中使用 CGRectIntersectsRect 檢查子彈的邊框和屏幕矩形,任何完全移出屏幕的子彈都會被標記為重用: ~~~ // 子彈離開屏幕后,設為不可見 ?if (CGRectIntersectsRect([self boundingBox], screenRect) ==NO) { …… } ~~~ screenRect變量出于方便和性能的原因,被存儲為static 變量,因此它能被其他類訪問,并不需要每次使用的時候創建。static 變量在類實現文件中聲明并有效,比如 screenRect。它們就像類的全局變量,任何類實例都可以讀取和修改。成員變量則不同,它們只存在于每個實例對象中。因為屏幕尺寸在游戲期間永遠不會變,所有的子彈都需要用到它,把它存儲為所有實例的static變量顯然是行得通的。第一個實例負責給 screeenRect 賦值。 CGRectIsEmpty 方法負責檢查 screenRect 變量是否未初始化——因為是static變量,只需要初始化一次就行了。 ~~~ static CGRect screenRect; ...... // 確保只初始化一次 if (CGRectIsEmpty(screenRect)) { CGSize screenSize = [[CCDirectorsharedDirector] winSize]; screenRect = CGRectMake(0, 0,screenSize.width, screenSize.height); } ~~~ 接下來,移除GameScene 類中原有的用于射擊子彈的代碼。此外,需要用初始化 BulletCache 來替換初始化 CCSpriteBatchNode (在GameScene 的 init 方法中): ~~~ BulletCache* bulletCache = [BulletCache node]; [self addChild:bulletCache z:1tag:GameSceneNodeTagBulletCache]; ~~~ 還需要為 bulletCache 添加一個訪問方法以便其他類通過GameScene訪問BulletCache實例: ~~~ -(BulletCache*) bulletCache { CCNode* node = [self getChildByTag:GameSceneNodeTagBulletCache];NSAssert([node isKindOfClass:[BulletCache class]], @"not aBulletCache"); return (BulletCache*)node; } ~~~ InputLayer 現在可以用BulletCache 發射子彈了。 子彈的位置、速度和所用的角色幀這些屬性, 應當在 InputLayer 的update方法里傳遞給射擊方法: ~~~ if (fireButton.active && totalTime> nextShotTime) { nextShotTime = totalTime + 0.5f; GameScene* game = [GameScenesharedGameScene]; Ship* ship = [game defaultShip]; BulletCache* bulletCache = [gamebulletCache]; // 射擊前設置 position, velocity h和 spriteframe CGPoint shotPos = CGPointMake(ship.position.x+ [ship contentSize].width * 0.5f, ship.position.y); float spread = (CCRANDOM_0_1() - 0.5f) *0.5f; CGPoint velocity = CGPointMake(1, spread); ?[bulletCache shootBulletAt:shotPos velocity:velocityframeName:@"bullet.png"]; } ~~~ 重構后的射擊過程添加了一些非常必要的靈活性。你可以設想一下,敵人現在可以使用同樣的代碼發射它們自己的子彈了。 ## 二、敵人 此刻,對于敵人我們僅有一個模糊的概念,它們是干什么的?它們的行為是什么?對于敵人,最重要的是——你永遠不知道他們該干什么。 就游戲而言,這意味著一切都要從頭開始,要策劃出你想讓敵人做的事情,從而分析需要編寫的代碼。與真實世界不同,你完全控制著你的敵人們。是不是覺得自己很偉大?但在你或者其他人感到好笑之前,你需要為統治世界想出一個計劃。 我創建了3種不同類型的敵人的圖片。這里,我只知道其中一個應該是Boss。看一眼下面的圖片,然后想象一下這些敵人分別能干些什么: ![](https://box.kancloud.cn/2016-05-04_572a0ba38ada0.gif) 在寫代碼之前,先了解一下這些敵人有哪些行為是共性的,這樣有些代碼只用編寫一次。代碼復用是最重要的編碼規范。我們先來看看敵人們都有哪些共性: ¥? 發射子彈 ¥? 何時何地發射子彈的判斷邏輯 ¥? 能被玩家的子彈擊中 ¥? 不能被其他敵人的子彈擊中 ¥? 能被多次擊中(有生命值) ¥? 有固定的行為和移動方式 ¥? 死亡時顯示特定的行為或動畫 ¥? 從屏幕以外進入屏幕后將會顯示 ¥? 當移出屏幕后將不再顯示 你可能注意到,上面有些特性也符合玩家飛船。飛船也可以射擊子彈,它也可能經受多次射擊;當它被摧毀時也應該呈現某個行為或動畫,它給人的感覺類似一個特殊的敵人。 掃描上述列表,會有3種實現方式。可以創建一個類,把飛船、敵人、Boss都包含在其中。代碼將是有選擇地執行部分代碼,這取決于敵人的類型。例如,射擊代碼可能為不同的類型提供不同的分支。對于對象有限的游戲,這是不錯的辦法——但它無法面對大規模的對象。隨著游戲中加入越來越多地對象,你的游戲代碼必將變得肥大臃腫。對這個類的任何部分進行修改,都會潛在地對敵人或者飛船的行為帶來不希望的影響。用一個變量——敵人類型來決定代碼執行路徑是一種古老的C 編程方式,不符合 O-C 的面向對象特性。 這種方式至今仍然非常有用,但一定要慎用。 第二種方式,是創建一個類層次。用一個Entity類作為基類,從它派生出一個飛船類、2個怪物類、1個Boss類。很多程序員常這樣干,對于游戲對象不多的情況這種方式也非常好用。但本質上,這和第一種方式沒什么不同。基類封裝了子類要用到的一些通用代碼,但不是全部代碼。當Entity類中的代碼開始基于某個子類的類型執行某個分支時,情況變得糟糕——跟第一種方法一樣了。如果小心一點,你應該確保把針對某種敵人的代碼放在某個子類里,但在修改的時候很容易會把很多改動放到Entity類里。 第3種方式,是使用組件編程。這意味著不同的代碼路徑從Entity類層次結構中分離出來,這部分代碼僅僅加到所需的子類中。比如一個“血槽”組件。基于組件的編程可以單獨寫成一本書,對于射擊游戲這類項目而言,這顯得有些殺雞用牛刀了,因此我會混合后面兩種方式一起使用,這里只是給出一個概念: 如何組合游戲對象而不是各自為政,以及這樣做的好處。 我想說明的是,不存在最好的編碼方式。選擇某種方式完全是主觀的,取決于個人經驗和偏好。如果你愿意隨著對手上開發的游戲的逐漸理解,不斷重構你的代碼庫,能運行的代碼比干凈的代碼更可取。經驗讓你不經過計劃就能做出這些決定,讓你能更快地創建更多復雜游戲。要想達到這個目的,從完成一個小游戲開始,然后慢慢地挑戰自己的極限。這是個需要學習的過程,很不幸的,在這個過程中你的學習興趣也很容易被好高騖遠消滅掉。為什么每個老練的游戲編程人員會告訴新人,從簡單入手,去重寫經典的電玩游戲比如俄羅斯方塊、帕克人、小行星。 ## 三、Entity類 Entity 類是繼承自 CCSprite,只包含了Ship類中的setPosition方法定義,以使所有的Entity 實例始終在屏幕內移動。我只對代碼做了一小點改動(其實就是如下面代碼所示的if語句,原來的代碼是沒有if語句的),屏幕外的對象可以移動到屏幕內,但一旦進入屏幕后,它們不能再離開屏幕區域。在這個射擊類游戲中,敵人不會從你身邊走開,而是站在屏幕中間為了演示一下EnemyCache,進行簡單的介紹。屏幕區域檢查只是簡單檢查一下sprite的邊框是否完全被屏幕邊框所包含,如果是的話,代碼將讓sprite始終保持在屏幕邊框內: ~~~ -(void) { } setPosition:(CGPoint)pos // 如果當前位置在屏幕外,則不需要讓位置調整到屏幕內 // 這會允許對象從屏幕外部移動到屏幕內部 if (CGRectContainsRect([GameScene screenRect], [selfboundingBox])) { ... ?[supersetPosition:pos]; } ~~~ ShipEntity類取代了Ship類。由于Entity類已經包含了setPosition方法,ShipEntity類只剩下了initWithShipImage方法。該方法的代碼沒有改變。 ## 四、EnemyEntity類 我們需要繼續深入到EnemyEntity類,首先是頭文件: ~~~ #import <Foundation/Foundation.h> #import"Entity.h" typedef enum{ EnemyTypeBreadman = 0, EnemyTypeSnake, EnemyTypeBoss, EnemyType_MAX, } EnemyTypes; @interface EnemyEntity : Entity { EnemyTypes type; } +(id) enemyWithType:(EnemyTypes)enemyType; +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType; -(void) spawn; @end ~~~ 沒有什么特別的。EnemyTypes 枚舉用于3種不同的敵人類型,EnemyType_MAX用于在遍歷時標志結束。EnemyEntity類使用了一個EnemyTypes變量存儲類型,因此我可以用switch命令基于敵人的類型構建分支語句。EnemyEntity的實現包含許多代碼,我會把它分成幾個主題,并只顯示相關的代碼。首先是initWithType方法: ~~~ -(id) initWithType:(EnemyTypes)enemyType { type = enemyType; NSString* frameName; NSString* bulletFrameName; int shootFrequency = 300; switch (type) { case EnemyTypeBreadman: frameName= @"monster-a.png"; bulletFrameName= @"candystick.png"; break; case EnemyTypeSnake: frameName= @"monster-b.png"; bulletFrameName= @"redcross.png"; shootFrequency= 200; break; case EnemyTypeBoss: frameName= @"monster-c.png"; bulletFrameName= @"blackhole.png"; shootFrequency= 100; break; default: [NSException exceptionWithName:@"EnemyEntityException" reason:@"unhandled enemytype" userInfo:nil]; } if((self = [super initWithSpriteFrameName:frameName])) { //Create the game logic components [self addChild:[StandardMoveComponent node]]; StandardShootComponent* shootComponent = [StandardShootComponent node]; shootComponent.shootFrequency= shootFrequency; shootComponent.bulletFrameName= bulletFrameName; [self addChild:shootComponent]; //enemies start invisible self.visible = NO; [self initSpawnFrequency]; } return self; } ~~~ 方法一開始是變量賦值,根據敵人的類型,使用switch語句為每種類型提供默認值:敵人的角色幀名以及子彈的角色幀名。switch的default分支拋出異常,因為其他類型在Enemytypes枚舉中未定義。這樣,如果你定義了一種新的敵人類型,但是如果它不會動,或者發射出了錯誤的子彈,那么你會得到一個錯誤警告:哈,你忘記修改某些東西了! 最后別忘了調用[super init…]方法,否則super無法正確初始化并導致一個奇怪的錯誤然后崩潰。 接下來創建了一個組件,并把它加到EnemyEntity中。后面我會訪問這個組件,在此你只需要知道StandardMoveComponent 能讓敵人移動并射擊。 把注意力放到initSpawnFrequency方法。 ~~~ -(void) initSpawnFrequency { // initialize how frequent the enemies willspawn if(spawnFrequency == nil) { spawnFrequency = [[CCArray alloc] initWithCapacity:EnemyType_MAX]; [spawnFrequency insertObject:[NSNumber numberWithInt:80] atIndex:EnemyTypeBreadman]; [spawnFrequency insertObject:[NSNumber numberWithInt:260] atIndex:EnemyTypeSnake]; [spawnFrequency insertObject:[NSNumber numberWithInt:1500] atIndex:EnemyTypeBoss]; //spawn one enemy immediately [self spawn]; } } +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType { NSAssert(enemyType < EnemyType_MAX, @"invalidenemy type"); NSNumber* number = [spawnFrequency objectAtIndex:enemyType]; return [number intValue]; } -(void) dealloc { [spawnFrequency release]; spawnFrequency = nil; [super dealloc]; } ~~~ 我們把每種類型的敵人的出場頻率記錄在靜態數組spawnFrequency里。第一個EnemyEntity實例負責初始化CCArray數組。CCArray不能存儲原始數據類型比如整型,因此使用了NSNumber類。使用insertObject方法而不用addObject方法是為了保證對象加入時的順序,同時別人看到這個枚舉值也映射了對應的敵人類型。 dealloc方法釋放了CCArray對象,并將其設為nil,這點非常重要。作為靜態變量,第一個EnemyEntity對象在運行其dealloc方法時會釋放spawnFrequency的內存,如果spawnFrequency不被設為nil,下一個EnemyEntity對象的dealloc方法將視圖再次釋放,這會“過度釋放”spawnFrequency對象,導致程序崩潰。如果spawnFrequency為nil,任何發給它的消息都會被忽略,包括release消息。 spawn方法用于“生成”一個游戲對象: ~~~ -(void) spawn { CCLOG(@"spawn enemy"); // Select a spawn location just outside theright side of the screen, with random y position CGRect screenRect = [GameScene screenRect]; CGSize spriteSize = [self contentSize]; float xPos = screenRect.size.width + spriteSize.width * 0.5f; float yPos = CCRANDOM_0_1() * (screenRect.size.height - spriteSize.height)+ spriteSize.height * 0.5f; self.position = CGPointMake(xPos, yPos); // Finally set yourself to be visible, this alsoflag the enemy as "in use" self.visible = YES; } ~~~ 因為EnemyCache用于統一創建所有的敵人,這里整個spawn 方法只是設定一個隨機數的y坐標,x坐標是在右側屏幕以外。visible屬性在其他地方會用到,尤其是在組件類中,用于判斷EnemyEntity當前是否已使用。如果visible為NO,它可以被“生出”并顯示,如果為YES,它就會按照固定的邏輯運行。 ## 五、EnemyCache類 從名字上看,這會讓你想到BulletCache類,它也持有了大量已初始化對象,以便快速和簡單地重用,減少了游戲時對象的創建、釋放動作,而這恰恰是導致游戲流暢性下降的原因之一。尤其是動作游戲,這種不流暢給玩家體驗帶來了災難性后果。以下是EnemyCache的頭文件。 ~~~ #import <Foundation/Foundation.h> #import "cocos2d.h" @interface EnemyCache : CCNode { CCSpriteBatchNode* batch; CCArray* enemies; int updateCount; } @end ~~~ CCSpriteBatchNode對象包含全部敵人角色(sprite),CCArray則儲存了每種敵人的列表。updateCount變量在每幀生成一個敵人時自動增加。init方法與BulletCache的init方法十分類似: ~~~ -(id) init { if((self = [super init])) { //從貼圖集緩存中得到圖片 CCSpriteFrame* frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"monster-a.png"]; batch = [CCSpriteBatchNode batchNodeWithTexture:frame.texture]; [self addChild:batch]; [self initEnemies]; [self scheduleUpdate]; } return self; } ? ~~~ 但initEnemies方法就復雜多了: ~~~ -(void) initEnemies { // 創建enemies 數組,用于存放每種類型的敵人 enemies = [[CCArray alloc] initWithCapacity:EnemyType_MAX]; // 有多少種敵人,就創建多少個數組 for (int i = 0; i < EnemyType_MAX; i++) { //根據敵人種類的不同,設置不同的數組容量。 int capacity; switch (i) { case EnemyTypeBreadman: capacity = 6; break; case EnemyTypeSnake: capacity = 3; break; case EnemyTypeBoss: capacity = 1; break; default: [NSException exceptionWithName:@"EnemyCacheException" reason:@"unhandled enemytype" userInfo:nil]; break; } //不需要alloc數組,當數組被加到enemies數組時會自動retain CCArray* enemiesOfType = [CCArray arrayWithCapacity:capacity]; [enemies addObject:enemiesOfType]; } for (int i = 0; i < EnemyType_MAX; i++) { CCArray* enemiesOfType = [enemies objectAtIndex:i]; int numEnemiesOfType = [enemiesOfType capacity]; for (int j = 0; j < numEnemiesOfType;j++) { EnemyEntity* enemy = [EnemyEntity enemyWithType:i]; [batch addChild:enemy z:0 tag:i]; [enemiesOfTypeaddObject:enemy]; } } } ~~~ 有意思的是,CCArray enemies 對象自身包含了多個CCArray對象,每種類型的敵人使用一個CCArray。這是一個典型的 2 維數組。enemies 變量需要用alloc 分配內存,否則initEnemies 方法一結束它的內存會被釋放。相反,enimies數組中的CCAray 元素對象不需要alloc,因為當它被add 到enemies數組中時會被自動retain。每種敵人所用的CCArray數組,其初始容量為該類型一次允許加到屏幕中的個數。每種敵人的CCArray數組使用addObject方法加到enemies數組。用這種方式可以創建層次深度。事實上,cocos2d結點層次結構也是通過在CCNode 類中定義一個CCArray* children成員變量來構建的。 我將enimies數組的創建和初始化分別放在在兩個單獨的循環體中,盡管它們其實也可以在一個循環中進行,但它們明顯是屬于不同的任務,應該保持分離——至于因此導致的性能上的額外開銷,是微乎其微的。 根據在CCArray初始化時的初始容量,相同數目的敵人被構建出來并加入到CCSpriteBatchNode中,然后又加到對應的某種敵人使用的CCArray中。通過CCSpriteBatchNode也能訪問到敵人,但單獨把這些敵人放在分開的數組中更方便處理,代碼列表如下所示: ~~~ -(void) spawnEnemyOfType:(EnemyTypes)enemyType { CCArray* enemiesOfType = [enemies objectAtIndex:enemyType]; EnemyEntity* enemy; CCARRAY_FOREACH(enemiesOfType, enemy) { //查找可重建的敵人,重用 if (enemy.visible== NO) { //CCLOG(@"spawn enemy type %i",enemyType); [enemy spawn]; break; } } } ? -(void) update:(ccTime)delta { updateCount++; ? for (int i = EnemyType_MAX- 1; i >= 0; i--) { int spawnFrequency = [EnemyEntity getSpawnFrequencyForEnemyType:i]; if (updateCount % spawnFrequency == 0) { [self spawnEnemyOfType:i]; break; } } } ~~~ update方法使計數器updateCount加1。這并不會多花費多少時間,但卻是值得的,因為他會使我們接下來更輕松一些。 For循環比較奇怪,循環變量i從EnemyType_MAX開始遞減,一直到i為負值。這個目的是為了讓EnemyTypes 更大的怪物更早出生。例如,當boss怪和蛇同時出現時,首先讓boss怪出生。否則會導致這樣的事情發生,蛇會和boss爭搶出生機會,甚至阻塞了Boss的出生。這個出生邏輯有一個副作用,我把它保留給你自己去解決,如果你要寫一個自己的射擊游戲,你可能不得不自己實現一些東西。 spawnFrequency被EnemyEntity 的getSpawnFrequncyForEnemyType方法所賦值。 ~~~ +(int) getSpawnFrequencyForEnemyType:(EnemyTypes)enemyType { NSAssert(enemyType < EnemyType_MAX, @"invalidenemy type"); NSNumber* number = [spawnFrequency objectAtIndex:enemyType]; return [number intValue]; } ~~~ 這個方法首先斷言enemyType是否是有效值。然后從spawnFrequency數組中取出指定類型的敵人的NSNumber對象并返回其intValue值。 回到update方法,接下來使用取模運算%,計算updateCount能否被spawnFrequency所整除,意思是只有updateCount數到指定的數時(updateCount是個計數器),某個怪才會降生。 spanEnemyOfType方法從enemies數組中取出對應的CCArray,然后只需要遍歷指定的類型的CCArray數組,而不用去遍歷整個CCSrpiteBatchNode: ~~~ -(void) spawnEnemyOfType:(EnemyTypes)enemyType { CCArray* enemiesOfType = [enemies objectAtIndex:enemyType]; EnemyEntity* enemy; CCARRAY_FOREACH(enemiesOfType, enemy) { //find the first free enemy and respawn it if (enemy.visible== NO) { //CCLOG(@"spawn enemy type %i",enemyType); [enemy spawn]; break; } } } ~~~ 如果找到一個visible為NO的怪,調用其spawn方法。如果所有的該類怪的visible都是YES,當前屏幕上該類怪的數目已經達到最大,不再產生這種類別的怪,這樣就限制了屏幕上同一種怪的數量。 ## 六、Component類 Component類在游戲邏輯中被視作插件。如果把一個component(組件)加在一個entity類,則該entity可以執行組件的行為:移動,射擊,動畫,顯示生命值等等。編寫組件的好處是它能自動工作,因為它們與父容器(CCNode)交互,并盡可能地不對父容器做出要求。有時候組件要求父容器必須是一個EnemyEntity類,但實際上你可以在任何類型的EnemyEntity(子類)上使用它。組件類可根據使用組件的類來配置。例如,這是一個在EnemyEntity中使用StandarShoortComponent組件的例子: ~~~ StandardShootComponent* shootComponent = [StandardShootComponent node]; shootComponent.shootFrequency= shootFrequency; shootComponent.bulletFrameName= bulletFrameName; [self addChild:shootComponent]; ~~~ ? shootFrequency和bulletFrameName變量是根據EnemyType來初始化的。把StandartShootComponent添加到EnemyEntity類,該類將會擁有射擊的能力。因為組件類未對父容器做任何限制,你甚至可以把組件加到ShipEntity,使玩家飛船以指定射速進行自動射擊。通過簡單地激活或失活射擊組件,你可以用很少的代碼實現給玩家更換武器的效果。你僅僅是把射擊代碼隔離出來,然后把組建植入游戲對象并設置一些參數而已。 讓武器失效并切換武器的邏輯很簡單。甚至,你可以把組件使用到其他游戲。組件在封裝可重用代碼時非常有用,在許多游戲引擎中組件是一種標準機制。如果你想進一步了解游戲組件,請到我的blog([www.learn-cocos2d.com/2010/06/prefer-composition-inheritance/](http://www.learn-cocos2d.com/2010/06/prefer-composition-inheritance/))。 StandardShootComponent的頭文件如下: ~~~ @interface StandardShootComponent : CCSprite { int updateCount; int shootFrequency; NSString* bulletFrameName; } @property (nonatomic) int shootFrequency; @property (nonatomic, copy) NSString* bulletFrameName; @end ~~~ 有兩件事情值得注意。首先StandardShootComponent派生自CCSprite,盡管它沒有使用任何貼圖紋理。因為CCSpriteBatchNode只能包含CCSprite對象,而所有的EnemyEntity對象都被加到了CCSpriteBatchNode,而且EnemyEntity的子節點,這些都是StandardShotComponent的作用對象。因此StandardShootComponent需要從CCSprite繼承以滿足CCSpriteBatchNode的要求。 第2是一個NSString 指針,bulletFrameName,用@property關鍵字封裝成了屬性。如果你足夠細心,應該發現在@property定義中的copy關鍵字。這說明只要給這個屬性賦值,將產生一個復制操作。這樣做對于確保這個字符串始終可用很重要, 因為字符串通常都是autorelease對象。我們也可以用retain對象,問題在于,如果源字符串被改變,這將影響到bulletFrameName,這可能不是我們希望的。 當然,copy關鍵字還意味著我們要負責在dealloc中釋放它,如下所示。 ~~~ @implementation StandardShootComponent @synthesize shootFrequency; @synthesize bulletFrameName; -(id) init { if((self = [super init])) { [self scheduleUpdate]; } return self; } -(void) dealloc { [bulletFrameName release]; [super dealloc]; } -(void) update:(ccTime)delta { if(self.parent.visible) { updateCount++; if (updateCount >= shootFrequency) { //CCLOG(@"enemy %@ shoots!",self.parent); updateCount = 0; GameScene* game = [GameScene sharedGameScene]; CGPoint startPos = ccpSub(self.parent.position, CGPointMake(self.parent.contentSize.width * 0.5f, 0)); [game.bulletCache shootBulletFrom:startPos velocity:CGPointMake(-2, 0) frameName:bulletFrameName]; } } } @end ~~~ 真正的射擊代碼首先要檢查父對象是否visible為YES,否則射擊代碼顯然不應該被調用。BulletCache發射子彈時使用組件bulletFrameName 屬性和固定的速度進行發射。 開始位置startPos并不是指組件自己的位置,而是使用父容器的位置和contentSize計算出來的:子彈位于角色的左邊。 對于常規的怪,一個startPos就足夠了,但對于Boss來說,用它的嘴或者鼻子來發射子彈,這才酷呢!我把這個工作也留給了你:為組件增加一個屬性,以便子彈的初始位置可以被設置。當然,你也可以創建一種單獨的BossShootComponent類,專門給Boss設計一種更復雜的射擊模式。StandardMoveComponents 也是一樣的, boss怪也可能需要在屏幕右邊的某個位置不停盤旋。 ## 七、擊中物體 幾乎忘記了——你其實是想向怪物們開火并擊中它們,不是嗎? BulletCache類是檢查子彈擊中物體的理想地點。我把方法加在了BulletCache中。實際上是3個方法,2個是public的,1個是private方法,如下所示。使用這兩個方法:isPlayerBulletCollidingWithRect和isEnemyBulletCollidingWithRect方法的目的是為了隱藏根據子彈的主類進行碰撞檢測的內部細節。 ~~~ -(bool) isPlayerBulletCollidingWithRect:(CGRect)rect { return [self isBulletCollidingWithRect:rect usePlayerBullets:YES]; } -(bool) isEnemyBulletCollidingWithRect:(CGRect)rect { return [self isBulletCollidingWithRect:rect usePlayerBullets:YES]; } -(bool) isBulletCollidingWithRect:(CGRect)rect usePlayerBullets:(bool)usePlayerBullets { bool isColliding = NO; Bullet* bullet; CCARRAY_FOREACH([batch children], bullet) { if (bullet.visible&& usePlayerBullets == bullet.isPlayerBullet) { if(CGRectIntersectsRect([bullet boundingBox],rect)) { isColliding = YES; //remove the bullet bullet.visible= NO; break; } } } return isColliding; } ~~~ ? 你也可以把usePlayerBullets 參數暴露給其他類,但這樣把這個參數由bool類型改變為enum類型時只會更難,一旦你想使用第3種子彈怎么辦? 只對看得見的子彈進行檢測,同時要檢查isPlayerBullet 屬性,確保怪物們不會被自己的子彈擊中。其實碰撞檢測是件簡單的事情,你可以使用CGRectIntersectsRect,如果子彈真的擊中了什么,子彈自身也應該“消失”。 EnemyCache類持有所有的EenemyEntity對象,這里也是調用方法去檢測是否有怪物被玩家擊中的好地方。現在EnemyCache類增加了checkForBulletCollisions方法(會由update方法來調用): ~~~ -(void) checkForBulletCollisions { EnemyEntity* enemy; CCARRAY_FOREACH([batch children], enemy) { if (enemy.visible) { BulletCache* bulletCache = [[GameScene sharedGameScene] bulletCache]; CGRect bbox = [enemy boundingBox]; if([bulletCache isPlayerBulletCollidingWithRect:bbox]) { //This enemy got hit ... [enemy gotHit]; } } } } ~~~ ? 在這里,很方便遍歷所有的怪物,并忽略那些當前不可見的。使用BulletCache的isPlayerBulletCollidingWithRect方法以及怪物的boundingBox屬性進行檢測,我們能快速地發現一個怪是否被玩家子彈擊中;如果擊中,就調用EnemyEntity的gotHist方法,該方法只是簡單地把怪變為不可見。 我把飛船被怪物子彈擊中的練習留給了你。你必須在ShipEntity方法中調用update方法,然后實現checkForBulletCollisions方法并在update方法中調用它。你還要改變isPlayerBulletCollidingWithRect方法和isEnemyBulletColligingWithRect方法,當子彈擊中時播放聲效。 ## 八、Boss的血槽 作為Boss,不應該一槍斃命。應該向玩家顯示boss 的生命值,當boss被擊中時血槽中的數值就減少一點。首先,需要在EnemyEntity類中增加一個hitPoints成員變量(即血點),用于表明怪物需要多少次擊中才會KO。initialHitPoints變量儲存怪物滿血狀態下的血點值,因為怪物被殺死后我們需要恢復它原來的血點(別忘記,我們的怪都是可以被“重用”的)。對頭文件所做的修改如下: ~~~ @interface EnemyEntity : Entity { EnemyTypes type; int initialHitPoints; int hitPoints; } @property (readonly, nonatomic) int hitPoints; ~~~ 為了表現血槽,我們需要一個組件類。很顯然這就是HealthbarComponent類: ~~~ @interface HealthbarComponent : CCSprite { } -(void) reset; @end ~~~ HealthComponent類的實現則比較有趣。HealthBarComponent 根據怪物的剩余血點更新它的scaleX屬性(這個scaleX來自于CCNode)。 ~~~ -(id) init { if((self = [super init])) { self.visible = NO; [self scheduleUpdate]; } return self; } -(void) reset { float parentHeight = self.parent.contentSize.height; float selfHeight = self.contentSize.height; self.position = CGPointMake(self.parent.anchorPointInPixels.x, parentHeight + selfHeight); self.scaleX = 1; self.visible = YES; } -(void) update:(ccTime)delta { if(self.parent.visible) { NSAssert([self.parent isKindOfClass:[EnemyEntity class]], @"nota EnemyEntity"); EnemyEntity* parentEntity = (EnemyEntity*)self.parent; self.scaleX = parentEntity.hitPoints/ (float)parentEntity.initialHitPoints; } else if (self.visible) { self.visible = NO; } } @end ~~~ ? 血槽可以根據父對象的visible屬性在可視/不可視之間切換。reset方法把血槽放到怪物角色的頂上。因為血點減少是通過修改scaleX屬性來顯示的,scaleX也應當被重置。 update方法中,當血槽的父對象是可視時,首先判斷父對象是不是EnemyEntity類,因為血槽組件要使用到在EnemyEntity中才有效的某些屬性,我們必須確保它的父類必須是EnemyEntity類。我把scaleX屬性修改為百分數值:用當前血點除以滿血點。因為不知道什么時候血點會變,我們只有在每一幀都進行這個計算,不管血點到底有沒有發生變化。這樣做有點性能上的浪費,對于復雜計算而言,最好是從EnemyEntity的onHit方法去調用血槽組件的方法。 在EnemyEntity的init方法中,如果怪物類型為EnemyTypeBoss,則把組件HealthbarComponent加到EnemyEntity對象。 注意:parentEntity.initialHitPoints被強制轉換為float,否則”/”是進行整數除法,這樣的結果永遠是0。將除數使用float類型就可以保證除法是小數點除法,以得到非0的小數。 ~~~ if (type == EnemyTypeBoss) { HealthbarComponent*healthbar = [HealthbarComponent spriteWithSpriteFrameName: @"healthbar.png"]; [self addChild:healthbar]; } ~~~ spawn方法進行了擴展,包括把血點重置為滿血,調用子組件中的所有血槽組件的reset方法(如果由多個的話)。我省略了對怪物類型的判斷,因為血槽是很通用的,可以被任何怪物用到。 ~~~ -(void) spawn { //CCLOG(@"spawn enemy"); // 出生地點選擇在屏幕右邊,y坐標值為隨機數 CGRect screenRect = [GameScene screenRect]; CGSize spriteSize = [self contentSize]; float xPos = screenRect.size.width + spriteSize.width * 0.5f; float yPos = CCRANDOM_0_1() * (screenRect.size.height - spriteSize.height)+ spriteSize.height * 0.5f; self.position = CGPointMake(xPos, yPos); // 出生后就表示看得見了 self.visible = YES; // 重置血點,因為我們重用的對象很可能才被打死 hitPoints = initialHitPoints; // 重置一些組件,如血槽 CCNode* node; CCARRAY_FOREACH([self children], node) { if ([node isKindOfClass:[HealthbarComponent class]]) { HealthbarComponent* healthbar = (HealthbarComponent*)node; [healthbarreset]; } } } ~~~ ? ## 九、結論 做出一個完整并優雅的游戲是一個很大的成果,包括大量的重構,修改代碼改進射擊以及允許更多的特性并讓它們和諧相處。本章,學習了BulletCache和EnemyCache類的作用,使用它們對某個類的所有實例進行管理,便于在一個地方集中訪問這些實例。同時起到一種“實例池”的作用,有助于改善性能。 Entity類層次示范了如何把你的類分離出來,而不需要每個游戲對象都設計一個類。使用組件類和cocos2d結點這樣的層次結構的好處在于,你可以把一些很特別的功能創建為即插即用的類。這有助于用復合的方式而非繼承的方式構造你的游戲對象。以這種方式編寫游戲邏輯能更“柔性”,同時代碼的復用性更好。最后,還學習了如何向怪物射擊,以及BulletCache和EnemyCache類如何以一種直接的方式完成這個目的。HealthbarComponent提供了一個組件編程的極好例子。 這個游戲到這里還有幾件事情等你完成。首先最主要的是,玩家從來不會被子彈擊中。可能你想為蛇加上一個血槽,或者為boss的行為寫一些特殊的移動和射擊組件。總之,這是一個開始編寫滾屏游戲的絕佳起點,需要的只是不斷去改進它。下一章,我將講如果使用粒子特效為這個射擊游戲增加炫目的視覺效果。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看