cocos2dx實例開發之經典坦克

小時候紅白機上玩的的經典90坦克,看起來簡單,做起來其實有點複雜,這裏用原版素材還原了一個簡版

預覽

在這裏插入圖片描述

工程結構

在這裏插入圖片描述
遊戲架構
在這裏插入圖片描述
包括場景:

  • 歡迎界面,主菜單
  • 遊戲場景

步驟

菜單場景

對於圖片,音樂,動畫提前做緩存,提高後面使用效率

// 預加載資源(暫且使用同步模式)
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("img/tank/tank.plist", "img/tank/tank.png");
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("img/item/item.plist", "img/item/item.png");
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("img/tank/blast.plist", "img/tank/blast/blast.png");

SimpleAudioEngine::getInstance()->preloadBackgroundMusic("sound/levelstarting.wav");
SimpleAudioEngine::getInstance()->preloadBackgroundMusic("sound/gamewin.wav");
SimpleAudioEngine::getInstance()->preloadBackgroundMusic("sound/gameover.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/bonus.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/brickhit.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/eexplosion.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/fexplosion.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/ice.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/life.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/moving.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/nmoving.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/shieldhit.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/shoot.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/steelhit.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/tbonushit.wav");

Animation* player_born_animation = Animation::create();
player_born_animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("shield1.png"));
player_born_animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("shield2.png"));
player_born_animation->setDelayPerUnit(0.2);
AnimationCache::getInstance()->addAnimation(player_born_animation, "player_born_animation");

Animation* enemy_born_animation = Animation::create();
for (int i = 1; i <= 4; i++)
    enemy_born_animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("born" + std::to_string(i) + ".png"));
enemy_born_animation->setDelayPerUnit(0.2);
AnimationCache::getInstance()->addAnimation(enemy_born_animation, "enemy_born_animation");

遊戲場景

場景中地圖、坦克、子彈、道具都是子元素,每個元素都有對應的行爲和狀態

數據結構

地圖

地圖是由tmx文件導入的,提前在tiled編輯器裏導出,每個方格是地圖塊的最小單位
方格類型包括:

  • 空白
  • 土磚
  • 鋼板
  • 草地

不同方格屬性不同,比如是否能讓坦克通過,是否可以被子彈破壞

// 加載遊戲地圖(tile地圖必須保證tmx文件跟圖片資源相對目錄)
std::string map_name = "img/map/Round" + std::to_string(round) + ".tmx";
initWithTMXFile(map_name);
//    tile_map->setScale(kScaleFactor); // this works to make map looks bigger

// print map info
Size map_size = getContentSize();
Size map_array = getMapSize();
// tmx地圖的方格必須用行列值重新計算,getTileSize()是不準確的
Size tile_size = Size(map_size.width / map_array.width, map_size.height / map_array.height);

由於地圖加載場景中,尺寸大小會有縮放,所以需要精確計算每個方塊的大小

玩家坦克

class Player : public Sprite
{
public:
    virtual bool init();
    
    CREATE_FUNC(Player);
    
public:
    void initWithType(PlayerType player_type);
    void setGameScene(GameScene* game_scene);
    void move(float tm);
    Bullet* shootSingle(); // 射擊一次,產生一顆子彈
    Vector<Bullet*> shootDouble(); // 射擊一次,產生雙子彈
    void fetchItem(ItemType item_type); // 拾取道具
    void destroy(); // 玩家over
    
public:
    void setSize(Size size);
    void setDirection(JoyDirection direction);
    JoyDirection m_head_direction; // 坦克朝向
    float m_bullet_interval;
    bool m_moving;
    PlayerStatus m_status;
    PlayerWeapon m_weapon;
    
private:
    GameScene* m_game_scene = nullptr; // like callback
    Size m_size;
};
  • 切換狀態
  • 移動
  • 變換方向
  • 拾取道具
  • 開炮(不同子彈類型)

敵方坦克

enum EnemyType
{
    NORMAL,  // 普通坦克
    ARMOR,   // 裝甲車
    SPEED    // 迅捷坦克
};

enum EnemyStatus
{
    ESIMPLE, // 普通
    ESHIELD  // 無敵
};
class Enemy : public Sprite
{
public:
    virtual bool init();
    
    CREATE_FUNC(Enemy);
    
    void initWithType(EnemyType enemy_type);
    void setSize(Size size);
    
public:
    void setDirection(JoyDirection direction);
    JoyDirection m_head_direction;
    void move(float tm);
    void changeDirection();
    Bullet* shoot();
    void hit();
    void die();
    int m_life;
    
    EnemyType m_type;
    bool m_moving;
    EnemyStatus m_status;
    
private:
    Size m_size;
    float m_speed;
};
  • 不同類型(普通、遊擊、裝甲)
  • 移動
  • 切換方向
  • 開炮

子彈

enum BulletType
{
    BASE, // 單子彈
    POWER // 雙子單
};
class Bullet : public cocos2d::Sprite
{
public:
    virtual bool init();
    void initWithDirection(JoyDirection direction, BulletType bullet_type = BASE); // 初始化方向和紋理
    CREATE_FUNC(Bullet);
    
public:
    BulletType m_type; // 子彈類型
    bool m_hit_flag; // 標記子彈是否已擊中
    
private:
    void move(float tm);
    JoyDirection m_direction;
};
  • 不同火力
  • 移動
  • 撞擊

道具

enum ItemType
{
    ACTIVE, // 帽子
    STAR,  // 火力星
    BOMB,   // 炸彈
    SHOVEL,  // 鏟子
    CLOCK,  // 定時
    MINITANK // 命
};

class Item : public Sprite
{
public:
    virtual bool init();
    
    CREATE_FUNC(Item);
    void initWithType(ItemType item_type);

public:
    ItemType m_type;
    
};
  • 不同類型(帽子、火力、炸彈、鏟子、定時、命)
  • 出現
  • 被拾取

虛擬搖桿

專門實現了一個搖桿和射擊的中間控制層,通過回調函數的方式施加於遊戲場景,其中搖桿控制是稍微複雜一點的
基本思路:確定好搖桿中心的情況下,通過三角函數關係計算跟隨觸點與座標橫軸的角度,根據角度範圍劃分具體的方向,另外,在判定射擊的時候要處理好左右屏和觸摸先後的順序

// 獲取以p1爲圓心,p2p1與x軸正方向的弧度值
float Joypad::calcRad(Point p1, Point p2)
{
    float xx = p2.x - p1.x;
    float yy = p2.y - p1.y;
    
    // 斜邊
    float xie = sqrt(pow(xx, 2) + pow(yy, 2));
    // yy >= 0 弧度在於 0 到 π 之間。(0~180°)
    // yy < 0 弧度在於 π 到 2π 之間。(180°~360°)
    float rad = yy >= 0 ? (acos(xx / xie)) : (PI * 2 - acos(xx / xie));
    return rad;
}

// 得到與角度對應的半徑爲R的圓上一座標點, 相對值
Vec2 Joypad::getAnglePosition(float R, float rad)
{
    return Point(R * cos(rad), R * sin(rad));
}

如圖所示
在這裏插入圖片描述

void Joypad::onTouchesMoved(const std::vector<Touch*>& touches, Event* event)
{
//    CCLOG("onTouchMoved");
    
    if (!m_can_move)
        return;
    
    Point visible_origin = Director::getInstance()->getVisibleOrigin();
    Size visible_size = Director::getInstance()->getVisibleSize();
    
    // 某一時刻可能是一個或兩個觸點
    Point point1 = touches.front()->getLocation();
    Point point2 = touches.back()->getLocation();
    
    Point wheel_center = m_wheel->getPosition();
    float wheel_radius = m_wheel->getContentSize().width / 2;
//    float stick_radius = m_stick->getContentSize().width / 2;
    
    // 區分左右觸點,可能是同一個
    Point left_point;
    Point right_point;
    if (point1.x < point2.x)
    {
        left_point = point1;
        right_point = point2;
    }
    else
    {
        left_point = point2;
        right_point = point1;
    }
    
    // 只有左觸點在左半邊屏才判斷搖桿
    if (left_point.x < visible_origin.x + visible_size.width / 2)
    {
        Point point = left_point;
        // 判斷兩個圓心的距離是否大於外圈半徑
        float distance = sqrt(pow(point.x - wheel_center.x, 2) + pow(point.y - wheel_center.y, 2));
        
        float rad = calcRad(wheel_center, point);
        if (distance >= wheel_radius)
        {
            // 搖桿中心不超出外圈範圍
            m_stick->setPosition(wheel_center + getAnglePosition(wheel_radius, rad));
        }
        else
            m_stick->setPosition(point); // 搖桿跟隨觸點
        
        // 換算成角度,根據鍵類型確定方向,將方向控制信號傳給遊戲場景
        float angle = rad * 180.0 / PI;
        if (m_type == KEY4)
        {
            // 加入控制死區,只有圓心偏移距離夠長才換方向
            JoyDirection direction;
            if (distance >= wheel_radius / 5)
            {
                // 靠近軸90度範圍
                if ((angle >= 0 && angle < 45) || (angle >= 315 && angle < 360))
                    direction = RIGHT;  // 右
                else if (angle >= 45 && angle < 135)
                    direction = UP;    // 上
                else if (angle >= 135 && angle < 225)
                    direction = LEFT;  // 左
                else if (angle >= 225 && angle < 315)
                    direction = DOWN;  // 下
            }
            else
                direction = NONE;
            
            // callback
            if (m_game_scene)
                m_game_scene->onEnumDirection(direction);
        }
        else if (m_type == KEY8)
        {
            // 加入控制死區,只有圓心偏移距離夠長才換方向
            JoyDirection direction;
            if (distance >= wheel_radius / 5)
            {
                // 靠近軸45度範圍
                if ((angle >= 0 && angle < 22.5) || (angle >= 337.5 && angle < 360))
                    direction = RIGHT; // 右
                else if (angle >= 22.5 && angle < 67.5)
                    direction = RIGHT_UP; // 右上
                else if (angle >= 67.5 && angle < 112.5)
                    direction = UP; // 上
                else if (angle >= 112.5 && angle < 157.5)
                    direction = LEFT_UP; // 左上
                else if (angle >= 157.5 && angle < 202.5)
                    direction = LEFT; // 左
                else if (angle >= 202.5 && angle < 247.5)
                    direction = LEFT_DOWN; // 左下
                else if (angle >= 247.5 && angle < 292.5)
                    direction = DOWN; // 下
                else if (angle >= 292.5 && angle < 337.5)
                    direction = RIGHT_DOWN; // 右下
            }
            else
                direction = NONE;
            
            // callback
            if (m_game_scene)
                m_game_scene->onEnumDirection(direction);
        }
        else if (m_type == KEYANY)
        {
            // callback
            if (m_game_scene)
                m_game_scene->onAngleDirection(angle);
        }
    }
}
  • 中間的stick要跟隨觸點,但是有界限
  • 觸點放開就自動歸位

玩家控制

玩家控制包括方向鍵移動和開火射擊

void GameScene::onEnumDirection(JoyDirection direction)
{
//    CCLOG("GameScene onEnumDirection: %d", direction);
    
    static JoyDirection pre_direction = NONE;
    // 只有方向改變時纔給玩家坦克發控制指令
    if (direction != pre_direction)
    {
        // TODO: 每次重置爲最近的整點方塊中心位置,減少被卡住的概率
        m_player1->setDirection(direction);
        pre_direction = direction;
    }
}

void GameScene::onFireBtn(bool is_pressed)
{
//    CCLOG("GameScene onFireBtn: %s", is_pressed ? "yes" : "no");
    
    static bool pre_press_status = false;
    if (is_pressed != pre_press_status)
    {
        // 調度子彈,同時考慮單發和i連發
        if (is_pressed)
        {
            // TODO: every moment make sure only limit number bullets in the screen
            // FIXME: timer would wait at least one delay interval, here shoot a bullet at one btn click
            if (m_player_bullets.empty())
            {
                if (m_player1->m_weapon == SINGLE_GUN)
                {
                    Bullet* bullet = m_player1->shootSingle();
                    addChild(bullet, kMapZorder);
                    m_player_bullets.pushBack(bullet);
                }
                else if (m_player1->m_weapon == DOUBLE_GUN)
                {
                    Vector<Bullet*> double_bullets = m_player1->shootDouble();
                    for (Bullet* bullet : double_bullets)
                    {
                        addChild(bullet, kMapZorder);
                        m_player_bullets.pushBack(bullet);
                    }
                }
            }
            
            schedule(schedule_selector(GameScene::emitPlayerBullet), m_player1->m_bullet_interval);
        }
        else
            unschedule(schedule_selector(GameScene::emitPlayerBullet));
            
        pre_press_status = is_pressed;
    }
}

以上是遊戲場景的幾個回調函數,供搖桿類調用控制玩家

  • 移動中如果遇到障礙就無法沿當前方向繼續移動
  • 移動遇到道具自動拾取並生效
  • 可以在移動中開火
  • 長按射擊可以連續開火
  • 每次只允許有限的子彈,下一發要等到前一發結束

敵方坦克行爲

敵方坦克隨機出現在地圖上方出生點,隨機切換方向,隨機開火,同一批次生成的坦克有數量限制,總坦克數量有限制

void GameScene::generateEnemy(float tm)
{
    // 最後剩下的則停掉坦克生成
    if (m_enemies.size() == m_enemy_count)
    {
        unschedule(schedule_selector(GameScene::generateEnemy));
        return;
    }
    
    // 當前還有完整的一批,則不產生
    if (m_enemies.size() >= kEnemyBatchTankCount)
        return;
    
    Size map_size = m_battle_field->getContentSize();
    Size map_array = m_battle_field->getMapSize();
    // tmx地圖的方格必須用行列值重新計算,getTileSize()是不準確的
    Size tile_size = Size(map_size.width / map_array.width, map_size.height / map_array.height);

    // 根據概率生成敵方坦克
    float tank_type_factor = CCRANDOM_0_1();
    EnemyType enemy_type;
    if (tank_type_factor < 0.6)
        enemy_type = NORMAL;
    else if (tank_type_factor >= 0.6 && tank_type_factor < 0.9)
        enemy_type = ARMOR;
    else
        enemy_type = SPEED;
    
    Enemy* enemy = Enemy::create();
    enemy->initWithType(enemy_type);
    enemy->setSize(tile_size * 2 * kTankSizeFactor);
    
    // 隨機生成位置,頂部三個空位
    float tank_pos_factor = CCRANDOM_0_1();
    if (tank_pos_factor <= 1.0 / 3)
        enemy->setPosition(m_battle_field->getPositionX() + tile_size.width,
                           m_battle_field->getPositionY() + map_size.height - tile_size.height);
    else if (tank_pos_factor >= 1.0 / 3 && tank_pos_factor < 2.0 / 3)
        enemy->setPosition(m_battle_field->getPositionX() + map_size.width / 2,
                           m_battle_field->getPositionY() + map_size.height - tile_size.height);
    else
        enemy->setPosition(m_battle_field->getPositionX() + map_size.width - tile_size.width,
                           m_battle_field->getPositionY() + map_size.height - tile_size.height);
    
    // 隨機生成方向
    float tank_direction_factor = CCRANDOM_0_1();
    if (tank_direction_factor < 0.25)
        enemy->setDirection(UP);
    else if (tank_direction_factor >= 0.25 && tank_direction_factor < 0.5)
        enemy->setDirection(DOWN);
    else if (tank_direction_factor >= 0.5 && tank_direction_factor < 0.75)
        enemy->setDirection(LEFT);
    else
        enemy->setDirection(RIGHT);
    
    // 根據定時決定初始是否可以移動
    if (m_is_clock)
        enemy->m_moving = false;
    
    addChild(enemy, kMapZorder);
    m_enemies.pushBack(enemy);
}
  • 固定時間間隔檢查調度
  • 移動中遇到障礙無法沿當前方向繼續移動
  • 可以在移動中開火
  • 不同坦克類型移動速度不同
  • 不同坦克射擊子彈頻率不同

子彈生成

子彈實際是由玩家或地方坦克射擊產生的,但是需要在場景中調度

void GameScene::emitPlayerBullet(float tm)
{
    // 小技巧,如果子彈消失了則快速發射一顆,所以子彈發射頻率會隨着碰撞調整
    if (m_player_bullets.empty())
    {
        if (m_player1->m_weapon == SINGLE_GUN)
        {
            Bullet* bullet = m_player1->shootSingle();
            addChild(bullet, kMapZorder);
            m_player_bullets.pushBack(bullet);
        }
        else if (m_player1->m_weapon == DOUBLE_GUN)
        {
            Vector<Bullet*> double_bullets = m_player1->shootDouble();
            for (Bullet* bullet : double_bullets)
            {
                addChild(bullet, kMapZorder);
                m_player_bullets.pushBack(bullet);
            }
        }
    }
}

void GameScene::emitEnemyBullet(float tm)
{
    for (Enemy* enemy : m_enemies)
    {
        // 不同的敵人坦克根據類型,發射子彈頻率不同
        float enemy_shoot_factor = CCRANDOM_0_1();
        if (enemy->m_type == NORMAL && enemy_shoot_factor >= 0.5
            || enemy->m_type == ARMOR && enemy_shoot_factor >= 0.2
            || enemy->m_type == SPEED && enemy_shoot_factor >= 0.7)
        {
            Bullet* bullet = enemy->shoot();
            addChild(bullet, kMapZorder);
            m_enemy_bullets.pushBack(bullet);
        }
    }
}
  • 固定時間間隔檢查調度
  • 射擊後自動飛行
  • 射出時出現在坦克頭部
  • 射出時的方向跟坦克方向相同
  • 碰撞到不同障礙物有對應行爲(普通子彈只能打土磚,火力子彈可以牀鋼板)

道具生成

道具是玩家射擊到裝甲車地方坦克時,根據概率隨機選擇類型出現在地圖任意位置

void GameScene::generateItem()
{
    SimpleAudioEngine::getInstance()->playEffect("sound/tbonushit.wav");
    
    Size map_size = m_battle_field->getContentSize();
    Size map_array = m_battle_field->getMapSize();
    // tmx地圖的方格必須用行列值重新計算,getTileSize()是不準確的
    Size tile_size = Size(map_size.width / map_array.width, map_size.height / map_array.height);
    
    // 隨機類型
    ItemType item_type;
    float item_type_factor = CCRANDOM_0_1();
    if (item_type_factor < 1.0 / 6)
        item_type = ACTIVE;
    else if (item_type_factor >= 1.0 / 6 && item_type_factor < 2.0 / 6)
        item_type = STAR;
    else if (item_type_factor >= 2.0 / 6 && item_type_factor < 3.0 / 6)
        item_type = BOMB;
    else if (item_type_factor >= 3.0 / 6 && item_type_factor < 4.0 / 6)
        item_type = SHOVEL;
    else if (item_type_factor >= 4.0 / 6 && item_type_factor < 5.0 / 6)
        item_type = CLOCK;
    else
        item_type = MINITANK;
    
    Item* item = Item::create();
    item->initWithType(item_type);
    
    // 隨機位置
    float item_posx_factor = CCRANDOM_0_1();
    float pos_x = m_battle_field->getPositionX() + (map_size.width - tile_size.width * 2) * item_posx_factor;
    float item_posy_factor = CCRANDOM_0_1();
    float pos_y = m_battle_field->getPositionY() + (map_size.height - tile_size.height * 2) * item_posy_factor;
    item->setPosition(pos_x, pos_y);
    
    addChild(item, kItemZorder);
    m_items.pushBack(item);
}
  • 固定時間間隔檢查調度
  • 會有閃爍特效
  • 不同類型被拾取後有對應效果

碰撞檢查

有多種類型的碰撞檢查

  • 判斷總部老鷹是否被擊毀
  • 判斷敵方坦克是否被射擊,死亡還是減血,掉落道具
  • 判斷玩家坦克是否被射擊
  • 判斷玩家坦克移動是否遇到障礙
  • 判斷敵方坦克移動是否遇到障礙
  • 判斷玩家子彈對於場景的碰撞破壞
  • 判斷敵方子彈對於場景的碰撞破壞

具體邏輯都卸載update函數,保證每幀都會檢測,及時反映

其中由於需要說明的是,對於地圖中不同磚塊被子彈碰撞或被坦克碰撞的邏輯,需要根據座標定位到方塊gid號,進而判斷類型來做對應的處理,另外如果拾取到鏟子道具,需要對地圖中土磚變換爲鋼板,做方塊類型的變更

bool isBulletCollide(Rect bounding_box, BulletType bullet_type); // 子彈的碰撞
bool isTankCollide(Rect bounding_box, JoyDirection direction); // 帶方向坦克的碰撞
bool isEagleHurt(Rect bounding_box);

void protectEagle(); // 用鋼板把老鷹圍起來
void unprotectEagle(); // 老鷹恢復爲土磚圍起來

元素管理

場景中的元素需要及時的回收和釋放,避免內存浪費

  • 出了地圖邊界的子彈銷燬
  • 碰撞的子彈銷燬
  • 被破壞的方塊銷燬
  • 被擊毀的地方坦克銷燬
  • 被擊毀的玩家坦克銷燬

遊戲狀態

如果地方坦克全部被擊毀,則遊戲勝利;如果玩家坦克命數用完或者總部老鷹被擊毀則遊戲結束;當前關卡通過後會進入下一關

void GameScene::gameWin()
{
    CCLOG("game win");
    
    SimpleAudioEngine::getInstance()->playBackgroundMusic("sound/gamewin.wav", false);
    
    // 切換關卡,通關後從第一關開始
    int next_round = m_round + 1; // 關卡提升
    if (next_round > kTotalRound)
        next_round = 1;
    
    // FIXME: should delay before next round
    Scene* scene = GameScene::createScene(next_round);
    TransitionScene* transition_scene = TransitionFade::create(0.0, scene);
    Director::getInstance()->replaceScene(transition_scene);
}

void GameScene::gameOver()
{
    CCLOG("game over");
    SimpleAudioEngine::getInstance()->playEffect("sound/gameover.wav", false);
    m_is_over = true;
    
    // 遊戲結束的標籤
    Point visible_origin = Director::getInstance()->getVisibleOrigin();
    Size visible_size = Director::getInstance()->getVisibleSize();
    
    Label *gameover_label = Label::createWithTTF("game over", "fonts/Marker Felt.ttf", 24);
    gameover_label->setColor(Color3B::WHITE);
    gameover_label->setPosition(visible_origin.x + visible_size.width / 2,
                                visible_origin.y - 30);
    addChild(gameover_label, kLevelSplashZorder); // 分數浮層在最上方
    
    // 播放動畫,飛入畫面
    auto move_to = MoveTo::create(1.0, Point(visible_origin.x + visible_size.width / 2,
                                             visible_origin.y + visible_size.height / 2));
    gameover_label->runAction(move_to);
}

效果圖

在這裏插入圖片描述

在這裏插入圖片描述

後記

可以考慮擴展成雙人聯網合作模式
技術實現思路:

  • 通過局域網連接
  • 主玩家建立內嵌http websocket服務器接收從玩家的連接
  • 互相之間通過websocket長連接通信
  • 遊戲邏輯在服務端計算,圖形渲染在客戶端各自渲染

代碼

csdn:經典坦克
github:經典坦克

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章