使用cocos2d-x製作基於Tile地圖的遊戲:加入敵人和戰鬥(三)

本教程基於子龍山人翻譯的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重寫,加上我一些加工製作。教程中大多數文字圖片都是原作者和翻譯作者子龍山人,還有不少是我自己的理解和加工。感謝原作者的教程和子龍山人的翻譯。本教程僅供學習交流之用,切勿進行商業傳播。
子龍山人翻譯的Iphone教程地址:http://www.cnblogs.com/andyque/archive/2011/05/07/2039481.html
Iphone教程原文地址:http://geekanddad.wordpress.com/2010/06/22/enemies-and-combat-how-to-make-a-tile-based-game-with-cocos2d-part-3/

程序截圖:


這篇教程是《使用cocos2d-x製作基於Tile地圖的遊戲》系列教程的後續。如果你還沒有看過前面兩部分的教程,可以在我的博客上找到另外兩篇教程。
  在第二部分教程中,Ray教大家如何在地圖中製作可碰撞的區域,如何使用tile屬性,如何製作可以拾取的物品以及如何動態修改地圖、如何使用“Heads up display”來顯示分數。
  在這個教程中,我們將加入敵人,這樣的話,你的忍者就可以向它們扔飛鏢啦,同時還增加了勝利和失敗的遊戲邏輯。但是,首先,你得下載一些相關的資源文件。
  在這個教程中,我們將加入敵人,這樣的話,你的忍者就可以向它們扔飛鏢啦,同時還增加了勝利和失敗的遊戲邏輯。但是,首先,你得下載一些相關的資源文件
  這個zip文件裏面包含以下內容:
    1.一個敵人精靈
    2.一個忍者飛鏢
    3.兩張按鈕的圖片,在教程的後面有使用。

增加敵人
  到第二部分教程結束的時候,遊戲已經很酷了,但是它還不是一個完整的遊戲。你的忍者可以輕而易舉地四處遊蕩,想喫就喫。但是,什麼時候玩家會勝利或者失敗呢。我們不妨想象一下,有2個敵人在追殺你的忍者,那麼這個遊戲會顯得更加有趣。
敵人出現的位置點
  好了,回到Tiled軟件,然後打開你的Tile地圖(TileMap.tmx)。
  往對象層中加入一個對象,在player附近就行,但是不要太近,否則敵人一出現玩家就Game over了。這個位置將成爲敵人出現的位置點,把它命名爲“EnemySpawn1”。
     對象組(對象層中的所有對象組成一個對象組)中的對象被存儲在一個List<Dictionary>中,同時使用對象名字作爲value。這意味着每一個位置點必須有一個唯一的名字。儘管我們可以遍歷所有的Dictionary是否有value是以“EnemySpawn”開頭,但是這樣做效率很低下。相反,我們採用的做法是,使用一個屬性來表示,每個給定的對象代表一個敵人出現的位置點。


  給這個對象一個屬性“Enemy”,同時賦一個值1.如果你想在這個教程的基礎上擴展,並且增加其它的不同類型的敵人,你可以使用這些敵人的屬性值來表示不同類型的敵人。
  現在,製作6-10個這種敵人出現位置點對象,相應的它們離player的距離也要有一些不同。爲每一個對象定義一個“Enemy”屬性,並且賦值爲1.保存這張地圖並且更新到工程中。
  
  
開始創建敵人
  好了,現在我們將把敵人實際顯示到地圖上來。首先在TileMapLayer類中添加如下方法:
        void addEnemyAtXY(int x, int y)
        {
            CCSprite enemy = CCSprite.spriteWithFile("images/enemy1");
            enemy.position = new CCPoint(x, y);
            this.addChild(enemy);
        }
並且在init裏面的添加player的if語句後面添加:
                    if (item.ContainsValue("EnemySpawn1"))
                    {
                        int ex =  Convert.ToInt32(item["x"]);
                        int ey = Convert.ToInt32(item["y"]);
                        this.addEnemyAtXY(ex, ey);
                    }
  循環遍歷對象列表,判斷是否是敵人對象,如果是,則獲得它的x和y座標值,然後調用addEnemyAtXY方法把它們加入到合適的地方去。
  這個addEnemyAtXY方法非常直白,它僅僅是在傳入的X,Y座標值處創建一個敵人精靈。
  如果你編譯並運行,你會看到這些敵人出現在你之前在Tiled工具中設定的位置處,很酷吧!



  但是,這裏有一個問題,這些敵人很傻瓜,它們並不會追殺你的忍者。
使它們移動
  因此,現在我們將添加一些代碼,使這些敵人會追着我們的player跑。因爲,player肯定會移動,我們必須動態地改變敵人的運動方向。爲了實現這個目的,我們讓敵人每次移動10個像素,然後在下一次移動之前,先調整它們的方向。在TileMapLayer類中加入如下代碼:
        /// <summary>
        /// callback, starts another iteration of enemy movement
        /// </summary>
        /// <param name="sender"></param>
        void enemyMoveFinished(object sender)
        {
            CCSprite enemy = sender as CCSprite;
            this.animateEnemy(enemy);
        }


        /// <summary>
        /// a method to move enemy 10 pixls toward the player
        /// </summary>
        /// <param name="enemy"></param>
        void animateEnemy(CCSprite enemy)
        { 
            //speed of the enemy
            float acturalDuration = 0.3f;
            var actionMove = CCMoveBy.actionWithDuration(acturalDuration, CCPointExtension.ccpMult(
                CCPointExtension.ccpNormalize(
                CCPointExtension.ccpSub(player.position, enemy.position)), 10));
            var actionMoveDone = CCCallFuncN.actionWithTarget(this, enemyMoveFinished);
            enemy.runAction(CCSequence.actions(actionMove, actionMoveDone));
        }
並且在addEnemyAtXY方法最後添加:
this.animateEnemy(enemy);

  animateEnemy:方法創建兩個action。第一個action使之朝敵人移動10個像素,時間爲0.3秒。你可以改變這個時間使之移動得更快或者更慢。第二個action將會調用enemyMoveFinished:方法。我們使用CCSequence action來把它們組合起來,這樣的話,當敵人停止移動的時候就立馬可以執行enemyMoveFinished:方法就可以被調用了。在addEnemyAtX:Y:方法裏面,我們調用animateEnemy:方法來使敵人朝着玩家(player)移動。(其實這裏是個遞歸的調用,每次移動10個像素,然後又調用enemyMoveFinished:方法)

很簡潔!但是,但是,如果敵人每次移動的時候面部都對着player那樣是不是更逼真呢?只需要在animateEnemy:方法中加入下列語句即可:

            //immediately before creating the actions in animateEnemy
            //rotate to face the player 
            CCPoint diff = CCPointExtension.ccpSub(player.position, enemy.position);
            double angleRadians = Math.Atan((double)(diff.y / diff.x));
            float angleDegrees = MathHelper.ToDegrees((float)angleRadians);
            float cocosAngle = -1 * angleDegrees;
            if (diff.x < 0)
            {
                cocosAngle += 180;
            }
            enemy.rotation = cocosAngle;

這個代碼計算每次玩家相對於敵人的角度,然後旋轉敵人來使之面朝玩家。



滿屏的怪物,似乎怪物設置得有點多了。。


忍者飛鏢
  已經很不錯了,但是玩家是一個忍者啊!他應該要能夠保護他自己!
  我們將向遊戲中添加模式(modes)。模式並不是實現這個功能的最好方式,但是,它比其他的方法要簡單,而且這個方法在模擬器下也能運行(因爲並不需要多點觸摸)。因爲這些優點,所以這個教程裏面,我們使用這種方法。首先將會建立UI,這樣的話玩家可以方便地在“移動模式”和“擲飛鏢”模式之間進行切換。我們將增加一個按鈕來使用這個功能的轉換。(即從移動模式轉到擲飛鏢模式)。
  現在,我們將增加一些屬性,使兩個層之間可以更好的通信。在TileMapHud類裏面增加如下代碼:
        int _mode = 0;
        public int mode { 
            get {
                return _mode;
            } 
        }
        /// <summary>
        /// callback for the button
        /// mode 0 = moving mode
        /// mode 1 = ninja star throwing mode
        /// </summary>
        /// <param name="sender"></param>
        void projectileButtonTapped(object sender)
        {
            if (_mode == 1)
            {
                _mode = 0;
            }
            else
                _mode = 1;
        }


    //in the init method
         CCMenuItem on = CCMenuItemImage.itemFromNormalImage(@"images/projectile-button-on", @"images/projectile-button-on");
            CCMenuItem off = CCMenuItemImage.itemFromNormalImage(@"images/projectile-button-off", @"images/projectile-button-off");
            CCMenuItemToggle toggleItem = CCMenuItemToggle.itemWithTarget(this, projectileButtonTapped, on, off);
            CCMenu toggleMenu = CCMenu.menuWithItems(toggleItem);
            toggleMenu.position = new CCPoint(100, 32);
            this.addChild(toggleMenu);
上面我們做了什麼呢,就是添加了一個變量mode作爲判斷所用。另外添加一個button。

編譯並運行。這時會在左下角出現一個按鈕,並且你可以打開或者關閉之。但是這並不會對遊戲造成任何影響。我們的下一步就是增加飛鏢的發射。





發射飛鏢
  接下來,我們將添加一些代碼來檢查玩家當前處於哪種模式下面,並且在用戶點擊屏幕的時候影響不同的事件。如果是移動模式則移動玩家,如果是射擊模式,則擲飛鏢。在ccTouchEnded方法裏面增加下面代碼:
if (hud.mode ==0) {
// old contents of ccTouchEnded:withEvent:
} else {
// code to throw ninja stars will go here
}
  這樣可以使得移動模式下,玩家只能移動。下一步就是要添加代碼使忍者能夠發射飛鏢。在else部分增加,在增加之前,先在TileMapLayer類中添加一些清理代碼:
        void projectileMoveFinished(object sender)
        {
            CCSprite sprite = sender as CCSprite;
            this.removeChild(sprite, true);
        }
好了,看到上面的else部分的註釋了嗎:
// code to throw ninja stars will go here
在上面的註釋後面添加下面的代碼:
//code to throw ninja stars will go here
                //Find where the touch is 
                CCPoint touchLocation = touch.locationInView(touch.view());
                touchLocation = CCDirector.sharedDirector().convertToGL(touchLocation);
                touchLocation = this.convertToNodeSpace(touchLocation);


                //Create a projectile and put it at the player's location
                CCSprite projectile = CCSprite.spriteWithFile(@"images/Projectile");
                projectile.position = new CCPoint(player.position.x, player.position.y);
                this.addChild(projectile);


                //Determine where we wish to shoot the projectile to
                float realX;


                //Are we shooting to left or right?
                CCPoint diff = CCPointExtension.ccpSub(touchLocation, player.position);
                if (diff.x > 0)
                {
                    realX = tileMap.MapSize.width * tileMap.TileSize.width + projectile.contentSize.width / 2;
                }
                else
                    realX = -tileMap.MapSize.width * tileMap.TileSize.width - projectile.contentSize.width / 2;
                float ratio = diff.y / diff.x;
                float realY = (realX - projectile.position.x) * ratio + projectile.position.y;
                CCPoint realDest = new CCPoint(realX, realY);


                //Determine the length of how far we're shooting
                float offRealX = realX - projectile.position.x;
                float offRealY = realY - projectile.position.y;
                double length = Math.Sqrt((double)(offRealX * offRealX + offRealY * offRealY));
                float velocity = 480 / 1;//480pixls/1sec
                float realMoveDuration = (float)length / velocity;


                //Move projectile to actual endpoint
                var actionMoveDone = CCCallFuncN.actionWithTarget(this, projectileMoveFinished);
                projectile.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(realMoveDuration,realDest),actionMoveDone));
  這段代碼會在用戶點擊屏幕的方向發射飛鏢。對於這段代碼的完整的細節,可以查看我的另一個文章《用cocos2d-x做一個簡單的windows phone 7遊戲(一)》。當然,查看原作者的文章後面的註釋會更加清楚明白一些。

  projectileMoveFinished:方法會在飛鏢移動到屏幕之外的時候移除。這個方法非常關鍵。一旦我們開始做碰撞檢測的時候,我們將要循環遍歷所有的飛鏢。如果我們不移除飛出屏幕範圍之外的飛鏢的話,這個存儲飛鏢的列表將會越來越大,而且遊戲將會越來越慢。編譯並運行工程,現在,你的忍者可以向敵人投擲飛鏢了。


碰撞檢測
  接下來,就是當飛鏢擊中敵人的時候,要把敵人銷燬。
那麼,我們應該怎麼做呢。我倒想用BOX2D來做碰撞檢測,不過常規的碰撞檢測也是可以的。這個就提供兩個方案吧。
先添加一個類到classes文件夾。命名爲GameOverScene,並且使之繼承於CCScene。
修改代碼如下;
       public GameOverScene(bool isWin)
        {
            string winMsg;
            if (isWin)
            {
                winMsg = "YOU WIN!";
            }
            else
                winMsg = "YOU LOSE!";
            CCLabelTTF label = CCLabelTTF.labelWithString(winMsg, "Arial", 32);
            label.position = new CCPoint(400, 300);
            this.addChild(label);
            this.runAction(CCSequence.actions(CCDelayTime.actionWithDuration(3.0f), CCCallFunc.actionWithTarget(this, gameOverDone)));
        }


        void gameOverDone()
        {
            TileMapScene pScene = new TileMapScene();
            CCDirector.sharedDirector().replaceScene(pScene);
        }
    }
這樣,就完成了GameOverScene了。上面的代碼很清楚了。就是添加一個label,並且在延時3秒後跳轉到遊戲界面。


PART1:BOX2D檢測
  這個部分,我們用BOX2D來做碰撞檢測,如果你對BOX2D不瞭解,可以看我博客裏面的<cocos2d-x for wp7>在cocos2d-x裏面使用BOX2D<cocos2d-x for wp7>使用box2d來做碰撞檢測(且僅用來做碰撞檢測)
  具體添加代碼方法看<cocos2d-x for wp7>使用box2d來做碰撞檢測(且僅用來做碰撞檢測)。其實是差不多的。我在這裏大概說下我做了什麼,後面也會提供一個示例代碼。

  首先,我添加了和碰撞檢測文章中一樣的聲明,世界的創建等操作,爲player,projectile,enemy都添加了tag以便碰撞檢測,並且也添加了一樣的addBoxBodyForSprite方法。tick方法。在碰撞檢測上面做了一樣的操作。當player和enemy碰撞的時候跳轉到GameOverScene顯示YOU LOSE。在setPlayerPosition裏面蒐集食物處添加檢測,如果蒐集數爲8,就跳轉顯示YOU WIN。(我數了下我地圖上的個數爲8)。

  這樣,就完成了。BOX2D做檢測還真是方便。示例代碼下載:http://dl.dbank.com/c0rr5l1b7p


PART2:普通方式檢測

   普通方式就是指用精靈的position和contentSize生成一個矩形做檢測。
  那麼,先往TileMapLayer類裏面添加變量聲明:
        List<CCSprite> enemies;
        List<CCSprite> projectiles;
然後在init裏面初使化list:
            //init the list
            enemies = new List<CCSprite>();
            projectiles = new List<CCSprite>();
然後在addEnemyAtXY方法的結尾添加如下代碼:
enemies.Add(enemy);
接着,在TileMapLayer類中添加如下代碼:

        void testCollision(float dt)
        {
            List<CCSprite> projectilesToDelete = new List<CCSprite>();
            List<CCSprite> targetToDelete = new List<CCSprite>();
            //iterate through projectiles
foreach (var projectile in projectiles)
            {
                CCRect projectileRect = new CCRect(
                    projectile.position.x - projectile.contentSize.width / 2,
                    projectile.position.y - projectile.contentSize.height / 2,
                    projectile.contentSize.width,
                    projectile.contentSize.height);
                
                //iterate through enemies,see if any intersect with current projectile
                foreach (var target in enemies)
                {
                    CCRect targetRect = new CCRect(
                        target.position.x - target.contentSize.width / 2,
                        target.position.y - target.contentSize.width / 2,
                        target.contentSize.width,
                        target.contentSize.height);
                    if (CCRect.CCRectIntersetsRect(projectileRect,targetRect))
                    {
                        targetToDelete.Add(target);
                    }
                }


                //delete all hit enemies
                foreach (var target in targetToDelete)
                {
                    enemies.Remove(target);
                    this.removeChild(target,true);
                }
                if (targetToDelete.Count > 0)
                {
                    //add the projectile to the list of ones to remove
                    projectilesToDelete.Add(projectile);
                }
                targetToDelete.Clear();
            }


            //remove all the projectiles that hit
            foreach (var projectile in projectilesToDelete)
            {
                projectiles.Remove(projectile);
                this.removeChild(projectile, true);
            }
            projectilesToDelete.Clear();
        }

然後添加以下代碼:
// at the end of the launch projectiles section of ccTouchEnded:withEvent:
projectiles.Add(projectile);


// at the end of projectileMoveFinished:
projectiles.Remove(sprite);


//in the init
this.schedule(testCollision);

  上面的所有的代碼,關於具體是如何工作的,可以在我的博客上查找《用cocos2d-x做一個簡單的windows phone 7遊戲(一)》教程。當然,原作者的文章註釋部分的討論更加清晰,所以我翻譯的教程,也希望大家多討論啊。代碼儘量自己用手敲進去,不要爲了省事,alt+c,alt+v,這樣不好,真的!
  好了,現在可以用飛鏢打敵人,而且打中之後它們會消失。現在讓我們添加一些邏輯,使得遊戲可以勝利或者失敗吧!
添加一個方法:
        void gameOver(bool isWin)
        {
            GameOverScene gameOverScene = new GameOverScene(isWin);
            CCDirector.sharedDirector().replaceScene(gameOverScene);
        }


 然後在setPlayerPosition裏面蒐集食物處添加檢測,如果蒐集數爲8,就跳轉顯示YOU WIN。(我數了下我地圖上的個數爲8)。


                        if (numCollected == 2)
                        {
                            gameOver(true);
                        }
就這個教程而言,我們的玩家只要有一個敵人碰到他,遊戲是結束了。在類的testCollision方法中添加以列循環:
            foreach (var target in enemies)
            {
                CCRect targetRect = new CCRect(
                    target.position.x - target.contentSize.width / 2,
                    target.position.y - target.contentSize.width / 2,
                    target.contentSize.width,
                    target.contentSize.height);
                if (CCRect.CCRectContainsPoint(targetRect, player.position))
                {
                    gameOver(false);
                }
            }
編譯並運行,得到預期效果。


  示例代碼:http://dl.dbank.com/c0xy82fc3w


接下來怎麼做?
建議:
增加多個關卡
增加不同類型的敵人
在Hud層中顯示血條和玩家生命
製作更多的道具,比如加血的,武器等等
一個菜單系統,可以選擇關卡,關閉音效,等等
使用更好的用戶界面,來使遊戲畫面更加精美,投擲飛鏢更加瀟灑。
另外,繼續:<cocos2d-x for wp7>使用cocos2d-x製作基於Tile地圖的遊戲:不一樣的戰鬥(回合制戰鬥)(四)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章