cocos2dx實例開發之飛機大戰

曾經,微信裏面可以玩一個打飛機的小遊戲,很有趣,後來又沒有了,這裏基於原版素材寫了一個高仿微信打飛機的小遊戲

預覽

在這裏插入圖片描述

工程結構

環境

  • Mac os Mojave
  • xcode 7.0
  • cocos2dx 3.17

代碼目錄
在這裏插入圖片描述

遊戲架構
在這裏插入圖片描述
主要包括以下場景

  • 主菜單
  • 遊戲(天空、玩家、敵機、子彈、道具)

步驟

菜單場景

遊戲主菜單界面,進入遊戲的入口界面

bool MainMenuScene::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Scene::init() )
    {
        return false;
    }

    auto visible_size = Director::getInstance()->getVisibleSize();
    auto visible_origin = Director::getInstance()->getVisibleOrigin();

	// 添加背景圖
    Sprite* background = Sprite::create("img/background.png");
    background->setContentSize(visible_size);
    background->setPosition(visible_origin.x + visible_size.width / 2,
                            visible_origin.y + visible_size.height / 2);
    addChild(background);
    
    Sprite* title = Sprite::create("img/title.png");
    title->setScale(1.5); // 尺寸適當調整
    title->setPosition(visible_origin.x + visible_size.width / 2,
                            visible_origin.y + visible_size.height / 2 + 40);
    addChild(title);

    // 添加按鈕
    Button* start_btn = Button::create("img/game_resume_nor.png", "img/game_resume_pressed.png", "img/game_resume_nor.png"); // 指定好各種狀態圖的按鈕
    start_btn->setScale(1.5); // 尺寸適當調整
    start_btn->setPosition(Vec2(visible_origin.x + visible_size.width / 2,
                                visible_origin.y + visible_size.height / 2));
    start_btn->addTouchEventListener([&](Ref* sender, Widget::TouchEventType type){
        // 按鈕點擊事件
        switch (type)
        {
            case ui::Widget::TouchEventType::BEGAN:
                // 播放音效
                SimpleAudioEngine::getInstance()->playEffect("sound/button.wav");
                break;
            case ui::Widget::TouchEventType::ENDED:
            {
                // 切換到主遊戲場景
                auto game_scene = GameScene::createScene();
                TransitionScene* transition_scene = TransitionFade::create(0.5, game_scene);
                Director::getInstance()->replaceScene(transition_scene);
            }
                break;
            default:
                break;
        }
    });
    addChild(start_btn);
    
    return true;
}

遊戲場景

遊戲場景就是主要的遊戲邏輯,控制玩家飛機發射子彈攻擊敵機,拾取道具等

數據結構

在場景中,玩家、敵機、子彈、道具、天空背景都是不同的對象,分別具有不同的行爲

玩家

class Player : public cocos2d::Sprite
{
public:
    virtual bool init();
    
    CREATE_FUNC(Player);
    
public:
    Bullet* shootSingle(); // 射擊一次,產生一顆子彈
    cocos2d::Vector<Bullet*> shootDouble(); // 射擊一次,產生雙子彈
    void fetchWeapon(WeaponType weapon_type); // 拾取道具
    void destroy(); // 玩家over
    
    BulletType m_bullet_type; // 根據子彈類型改變子彈
};
  • 射擊(不同子彈類型)
  • 拾取道具
  • 被摧毀

子彈

enum BulletType
{
    BASE, // 單子彈
    POWER // 雙子單
};

class Bullet : public cocos2d::Sprite
{
public:
    virtual bool init();
    void initWithType(BulletType bullet_type); // 根據子彈類型設置紋理
    CREATE_FUNC(Bullet);
    void pauseMove();
    void resumeMove();
    
public:
    int m_kill_hp; // 子彈的殺傷力
    bool m_hit_flag; // 標記子彈是否已擊中
    
private:
    void move(float tm);
    float m_speed;
};
  • 不同類型
  • 子彈飛行
  • 不同的速度
  • 不同的殺傷力

敵機

enum EnemyType
{
    SMALL,  // 小飛機
    MEDIUM, // 中等飛機
    BIG     // 大飛船
};

class Enemy : public cocos2d::Sprite
{
public:
    virtual bool init();
    
    CREATE_FUNC(Enemy);
    void pauseMove();
    void resumeMove();
    
    void initWithType(EnemyType enemy_type);
    void move(float tm);
    void hit(int reduce_hp); // 敵機被子彈打中
    void die(); // 敵機死亡
    
    int m_hp; // 敵機總血量
	EnemyType m_type;
    
private:
    float m_speed;
};
  • 不同類型
  • 敵機飛行
  • 不同的速度
  • 被子彈擊中
  • 不同的生命值
  • 減生命值
  • 被摧毀

道具

enum WeaponType
{
    AMMO,
    BOMB
};

class Weapon : public cocos2d::Sprite
{
public:
    virtual bool init();
    
    CREATE_FUNC(Weapon);
    void initWithType(WeaponType weapon_type);
    void pauseMove();
    void resumeMove();
    
    WeaponType m_type;
    
private:
    void move(float tm);
};
  • 不同類型
  • 道具飛行

背景滾動

天空背景實際上只有一張圖,構造兩張圖循環移動,產生天空無限移動的效果

  • 定時器調度移動
  • 在關鍵位置兩張圖位移重置
bool SkyBackground::init()
{
    if (!Node::init())
        return false;
    
    Size visible_size = Director::getInstance()->getVisibleSize();
    Point visible_origin = Director::getInstance()->getVisibleOrigin();
    
    // 增加兩張背景圖,循環交替,構造連續滾動效果
    m_background1 = Sprite::create("img/background.png");
    m_background1->setContentSize(visible_size); // 與屏幕適應
    m_background1->setAnchorPoint(Point::ZERO); // 錨點設爲左下座標點,方便計算
    m_background1->setPosition(visible_origin); // 一定要設置初始位置
    addChild(m_background1);
    m_background2 = Sprite::create("img/background.png");
    m_background2->setContentSize(visible_size);
    m_background2->setAnchorPoint(Point::ZERO);
    m_background2->setPosition(visible_origin.x, visible_origin.y + visible_size.height);
    addChild(m_background2);
    
    // 調度兩張圖的移動
    schedule(schedule_selector(SkyBackground::backgroundRotate), kFrameUpdateInterval); // delta time smaller, move more smooth
    
    return true;
}

void SkyBackground::backgroundRotate(float tm)
{
    Size visible_size = Director::getInstance()->getVisibleSize();
    Point visible_origin = Director::getInstance()->getVisibleOrigin();
    
    // 背景2在背景1上方無縫銜接,當背景2到達底部時候,將背景1重新歸位
    m_background1->setPositionY(m_background1->getPositionY() - 0.3); // rotate speed
    m_background2->setPositionY(m_background1->getPositionY() + visible_size.height);
    if (m_background2->getPositionY() <= visible_origin.y)
        m_background1->setPositionY(visible_origin.y);
}

玩家控制

玩家飛機根據觸摸移動進行相應的移動

  • 伴隨動畫
  • 跟隨觸摸位置
  • 產生射擊子彈
bool GameScene::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event)
{
    if (m_is_over)
        return true;
    
    CCLOG("onTouchBegan");
    m_pretouch_pos = touch->getLocation();
    m_preplayer_pos = m_player->getPosition();
    
    return true;
}

void GameScene::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event)
{
    if (m_is_over)
        return;
    CCLOG("onTouchMoved");
    Point current_touch_pos = touch->getLocation();
    Vec2 move_delta = current_touch_pos - m_pretouch_pos;
    m_player->setPosition(m_preplayer_pos + move_delta);
}

void GameScene::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event)
{
    if (m_is_over)
        return;
    
    CCLOG("onTouchEnded");
    m_pretouch_pos = kInitPoint;
}
Bullet* Player::shootSingle()
{
    // 發射子彈音效
    SimpleAudioEngine::getInstance()->playEffect("sound/bullet.wav");
    
    // 子彈從飛機頭部打出來
    Bullet* bullet = Bullet::create();
    bullet->initWithType(BulletType::BASE);
    bullet->setPosition(getPositionX(), getPositionY() + getContentSize().height / 2);
    return bullet;
}

Vector<Bullet*> Player::shootDouble()
{
    // 發射子彈音效
    SimpleAudioEngine::getInstance()->playEffect("sound/bullet.wav");
    
    Vector<Bullet*> double_bullets;
    // 子彈都從飛機頭部打出來,左右兩個子彈分佈在機頭
    Bullet* bullet_left = Bullet::create();
    bullet_left->initWithType(BulletType::POWER);
    bullet_left->setPosition(getPositionX() - getContentSize().width / 4, getPositionY() + getContentSize().height / 2);
    double_bullets.pushBack(bullet_left);
    
    Bullet* bullet_right = Bullet::create();
    bullet_right->initWithType(BulletType::POWER);
    bullet_right->setPosition(getPositionX() + getContentSize().width / 4, getPositionY() + getContentSize().height / 2);
    double_bullets.pushBack(bullet_right);
    
    return double_bullets;
}

void Player::fetchWeapon(WeaponType weapon_type)
{
    // 如果道具是子彈,且之前是單子彈,切換子彈,並持續一段時間
    if (m_bullet_type == BulletType::BASE && weapon_type == WeaponType::AMMO)
    {
        m_bullet_type = BulletType::POWER;
        // 播放音效
        SimpleAudioEngine::getInstance()->playEffect("sound/get_double_laser.wav");
        
        // 子彈有效時間,用lambda做單次定時器回調
        scheduleOnce([&](float delay){
            m_bullet_type = BulletType::BASE;
            SimpleAudioEngine::getInstance()->playEffect("sound/out_porp.wav");
        }, kPowerTime, "power_status");
    }
    else if (weapon_type == WeaponType::BOMB)
    {
        // 播放音效
        SimpleAudioEngine::getInstance()->playEffect("sound/get_bomb");
        
        // TODO: 儲存炸彈
    }
}

子彈生成

子彈是由場景通過定時器調度的,每次固定時間間隔根據當前玩家的子彈類型生成新的子彈

  • 子彈射出後就自動飛行
  • 子彈產生的位置都在玩家飛機頭部
void GameScene::generateBullet(float interval)
{
    if (m_is_over)
        return;
    
    // 由定時器觸發產生子彈,也可以由某個按鍵觸發
    // 根據玩家子彈狀態產生不同的子彈,加到場景和管理器
    if (m_player->m_bullet_type == BulletType::BASE)
    {
        Bullet* bullet = m_player->shootSingle();
        addChild(bullet, kBattleZorder);
        m_bullets.pushBack(bullet);
    }
    else if (m_player->m_bullet_type == BulletType::POWER)
    {
        Vector<Bullet*> double_bullets = m_player->shootDouble();
        for (Bullet* bullet : double_bullets)
        {
            addChild(bullet, kBattleZorder);
            m_bullets.pushBack(bullet);
        }
    }
}

敵機生成

敵機會隨機從頂部生成出來,也是由定時器調度

  • 位置隨機
  • 類型根據概率來,越厲害的飛機越少出現
  • 飛機出現後會飛行
  • 飛船有伴隨動畫
void GameScene::generateEnemy(float interval)
{
    if (m_is_over)
        return;
    
    Size visible_size = Director::getInstance()->getVisibleSize();
    Point visible_origin = Director::getInstance()->getVisibleOrigin();
    
    // 定時器觸發產生道具,根據概率生成不同類型
    // 加載到場景和管理器
    EnemyType enemy_type;
    float random_factor = CCRANDOM_0_1();
    if (random_factor >= 0.9)
        enemy_type = EnemyType::BIG;
    else if (random_factor >= 0.6 && random_factor < 0.9)
        enemy_type = EnemyType::MEDIUM;
    else
        enemy_type = EnemyType::SMALL;

    Enemy* enemy = Enemy::create();
    enemy->initWithType(enemy_type);
    
    // 生成隨機位置,但要保證敵機顯示完整,所以左右各留出半個敵機身位
    float random_x = enemy->getContentSize().width / 2 +
                    (visible_size.width - enemy->getContentSize().width) * CCRANDOM_0_1();
    
    enemy->setPosition(visible_origin.x + random_x,
                       visible_origin.y + visible_size.height + enemy->getContentSize().height / 2);
    addChild(enemy, kBattleZorder);
    m_enemies.pushBack(enemy);
}

道具生成

道具有子彈和炸彈兩種,由定時器調度

  • 道具出現位置隨機
  • 道具類型根據概率來,炸彈概率較小
  • 道具出現後會飛行

碰撞檢測

判斷敵機是否死亡、道具拾取、玩家是否被摧毀都要做碰撞檢測

  • 敵機碰撞到子彈後,先減生命值,生命值爲則死亡
  • 道具拾取後,如果是子彈,則玩家射擊的子彈專版爲加強版,維持一段時間,如果是炸彈,則立即炸燬場景裏所有存活的飛機
  • 玩家碰撞到任何敵機就被摧毀
  • 碰撞會有多種動畫伴隨
// --- 碰撞監測 ---
// 判斷玩家
for (Enemy* enemy : m_enemies)
{
    if (enemy->m_hp > 0 && enemy->getBoundingBox().intersectsRect(m_player->getBoundingBox()))
    {
        // 玩家飛機撞了
        m_is_over = true;
        m_player->destroy();

// 遊戲結束,處理後續(延遲等到玩家飛機t摧毀動畫結束)
        scheduleOnce([&](float delay){
            gameOver();
        }, 2.0, "game over");

        return;
    }
}

// 判斷敵機被子彈擊中
Vector<Bullet*> hit_bullets;
for (Bullet* bullet : m_bullets)
{
    for (Enemy* enemy : m_enemies)
    {
        // 注意,某個子彈撞擊了後要消失,並且不可再參與碰撞檢測
        if (!bullet->m_hit_flag
            && enemy->m_hp > 0
            && bullet->getBoundingBox().intersectsRect(enemy->getBoundingBox()))
        {
            enemy->hit(bullet->m_kill_hp);
            if (enemy->m_hp <= 0)
            {
	// 先更新分數,再移除飛機
	getScore(enemy->m_type);

                enemy->die(); // 這裏面做了對象銷燬
                m_enemies.eraseObject(enemy); // 這裏可以先於動畫放完就移出管理器
            }
            
            // 移除子彈, 先標記起來放到移除列表
            bullet->m_hit_flag = true;
            hit_bullets.pushBack(bullet);
        }
    }
}
// 這裏統一移除子彈,避免邊判斷邊移除導致的內存問題
for (Bullet* bullet : hit_bullets)
{
    m_bullets.eraseObject(bullet);
    bullet->removeFromParent();
}
hit_bullets.clear();

// 判斷拾取道具
for (Weapon* weapon : m_weapons)
{
    if (weapon->getBoundingBox().intersectsRect(m_player->getBoundingBox()))
    {
        if (weapon->m_type == WeaponType::AMMO)
            m_player->fetchWeapon(WeaponType::AMMO);
        else if (weapon->m_type == WeaponType::BOMB)
        {
            // 炸掉所有敵機
            SimpleAudioEngine::getInstance()->playEffect("sound/use_bomb.wav");
            CCLOG("before bomb enemy number = %d", m_enemies.size());
            for (Enemy* enemy : m_enemies)
            {
	// 先更新分數,再移除飛機
	getScore(enemy->m_type);

                enemy->m_hp = 0; // 先把血量置空
                enemy->die(); // 這裏面做了對象銷燬
            }
            m_enemies.clear(); // 一定要放在這裏統一清空
            CCLOG("after bomb enemy number = %d", m_enemies.size());
        }
        
        // 道具本身也要消失
        m_weapons.eraseObject(weapon);
        weapon->removeFromParent();
    }
}

元素管理

場景裏看不見的元素需要銷燬做內存回收

  • 監測子彈、敵機、道具如果出了平面就銷燬回收內存
  • 這種銷燬不需要播放動畫
// --- 元素管理 ---
for (Bullet* bullet : m_bullets)
{
    // 若飛行的子彈出了屏幕,則移除
    if (bullet->getPositionY() > visible_origin.y +
                                visible_size.height + bullet->getContentSize().height / 2)
    {
        m_bullets.eraseObject(bullet); // erase from vector should before itself cleanup
        bullet->removeFromParent();
    }
}
CCLOG("current bullets count: %d", m_bullets.size());

for (Enemy* enemy : m_enemies)
{
    // 若存活敵機出了平面,則移除
    if (enemy->getPositionY() < visible_origin.y - enemy->getContentSize().height / 2)
    {
        m_enemies.eraseObject(enemy);
        enemy->removeFromParent();
    }
}
CCLOG("alive enemy count: %d", m_enemies.size());

for (Weapon* weapon : m_weapons)
{
    // 若未拾取的道具出了屏幕,則移除
    if (weapon->getPositionY() < visible_origin.y - weapon->getContentSize().height / 2)
    {
        m_weapons.eraseObject(weapon);
        weapon->removeFromParent();
    }
}
CCLOG("unfetched weapon count: %d", m_weapons.size());

計分

在摧毀敵機或者炸掉敵機時都要更新遊戲分數

  • 不同類型敵機得分不同
  • 獲得里程碑分數會有成就音效
void GameScene::getScore(EnemyType enemy_type)
{
	// 根據不同敵機類別得不同分數
    static int phase = 0;
	switch (enemy_type)
	{
	case EnemyType::SMALL:
		m_score += kSmallPlaneScore;
		break;
	case EnemyType::MEDIUM:
		m_score += kMediumPlaneScore;
		break;
	case EnemyType::BIG:
		m_score += kBigPlaneScore;
		break;
	default:
		break;
	}

	// 如果分數達到一定階段,播放成就音效
    int new_phase = m_score / kAchievementScoreUnit;
	if (new_phase > phase)
    {
        SimpleAudioEngine::getInstance()->playEffect("sound/achievement.wav");
        phase = new_phase;
    }

	// 刷新UI
    m_score_label->setString(__String::createWithFormat("score: %d", m_score)->_string);
//    m_score_label->setString(StringUtils::format("score: %d", m_score)); // the same
}

遊戲狀態管理

遊戲結束、暫停、恢復

  • 遊戲結束出現新畫面
  • 遊戲暫停和恢復要保證場景裏所有元素同時動作
void GameScene::gameOver()
{
    CCLOG("player hit, game over");
    
    Size visible_size = Director::getInstance()->getVisibleSize();
    Point visible_origin = Director::getInstance()->getVisibleOrigin();
    
    // 停止背景音樂
    // FIXME: stop background music may cause crash
    if (SimpleAudioEngine::getInstance()->isBackgroundMusicPlaying())
        SimpleAudioEngine::getInstance()->pauseBackgroundMusic();
    
    // 停止所有的調度器
    unschedule(schedule_selector(GameScene::generateEnemy));
    unschedule(schedule_selector(GameScene::generateWeapon));
    unschedule(schedule_selector(GameScene::generateBullet));
    
    // 添加結束畫面
    Sprite* game_over_background = Sprite::create("img/gameover.png");
    game_over_background->setContentSize(visible_size);
    game_over_background->setAnchorPoint(Point::ZERO);
    game_over_background->setPosition(visible_origin);
    addChild(game_over_background, kGameoverZorder);
    
    Label* final_score_label = Label::createWithTTF(std::to_string(m_score), "fonts/Marker Felt.ttf", 14);
    final_score_label->setColor(Color3B::BLACK);
    final_score_label->setPosition(visible_origin.x + visible_size.width / 2,
                               visible_origin.y + visible_size.height / 2);
    addChild(final_score_label, kGameoverZorder);
    
    // 重新開始按鈕
    Button* restart_btn = Button::create("img/restart.png"); // 只添加一張背景圖按鈕
    restart_btn->setScale(1.5);
    restart_btn->setPosition(Vec2(visible_origin.x + visible_size.width / 2,
                                  visible_origin.y + visible_size.height / 2 - 30));
    restart_btn->addTouchEventListener([&](Ref* sender, Widget::TouchEventType type){
        // 按鈕點擊事件
        switch (type)
        {
            case ui::Widget::TouchEventType::BEGAN:
                // 播放音效
                SimpleAudioEngine::getInstance()->playEffect("sound/button.wav");
                break;
            case ui::Widget::TouchEventType::ENDED:
            {
                // 重載主遊戲場景
                auto game_scene = GameScene::createScene();
                TransitionScene* transition_scene = TransitionFade::create(0.5, game_scene);
                Director::getInstance()->replaceScene(transition_scene);
            }
                break;
            default:
                break;
        }
    });
    addChild(restart_btn, kGameoverZorder);
    
    // 結束遊戲按鈕
    Button* quit_btn = Button::create("img/quit.png"); // 只添加一張背景圖按鈕
    quit_btn->setScale(1.5);
    quit_btn->setPosition(Vec2(visible_origin.x + visible_size.width / 2,
                                  visible_origin.y + visible_size.height / 2 - 60));
    quit_btn->addTouchEventListener([&](Ref* sender, Widget::TouchEventType type){
        // 按鈕點擊事件
        switch (type)
        {
            case ui::Widget::TouchEventType::BEGAN:
                // 播放音效
                SimpleAudioEngine::getInstance()->playEffect("sound/button.wav");
                break;
            case ui::Widget::TouchEventType::ENDED:
            {
                // 返回到菜單場景
                auto main_menu_scene = MainMenuScene::createScene();
                TransitionScene* transition_scene = TransitionFade::create(0.5, main_menu_scene);
                Director::getInstance()->replaceScene(main_menu_scene);
            }
                break;
            default:
                break;
        }
    });
    addChild(quit_btn, kGameoverZorder);
}

void GameScene::gamePause()
{
    // 暫停背景音樂
    SimpleAudioEngine::getInstance()->pauseBackgroundMusic();
    
    // 暫停遊戲標誌
    m_is_pause = true;
    
    // 暫停所有調度器
    unschedule(schedule_selector(GameScene::generateEnemy));
    unschedule(schedule_selector(GameScene::generateWeapon));
    unschedule(schedule_selector(GameScene::generateBullet));
    
    // 暫停所有的元素移動
    m_sky_background->pauseRotate();
    for (Enemy* enemy : m_enemies)
        enemy->pauseMove();
    for (Bullet* bullet : m_bullets)
        bullet->pauseMove();
    for (Weapon* weapon : m_weapons)
        weapon->pauseMove();
}

void GameScene::gameResume()
{
    // 恢復背景音樂
    SimpleAudioEngine::getInstance()->resumeBackgroundMusic();
    
    // 恢復遊戲標誌
    m_is_pause = false;
    
    // 恢復所有調度器
    schedule(schedule_selector(GameScene::generateEnemy), kEnemyGenerateInterval);
    schedule(schedule_selector(GameScene::generateWeapon), kWeaponGenerateInterval);
    schedule(schedule_selector(GameScene::generateBullet), kBulletGenerateInterval);
    
    // 恢復所有的元素移動
    m_sky_background->resumeRotate();
    for (Enemy* enemy : m_enemies)
        enemy->resumeMove();
    for (Bullet* bullet : m_bullets)
        bullet->resumeMove();
    for (Weapon* weapon : m_weapons)
        weapon->resumeMove();
}

效果圖

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

代碼

csdn:飛機大戰
github:飛機大戰

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