1. 程式人生 > >射擊遊戲中準心與子彈彈道的探索

射擊遊戲中準心與子彈彈道的探索

前言

  現如今,精品遊戲競爭激烈,各大遊戲廠商都在爭相推出“3A”級品質的遊戲。而目前比較熱門的科幻、戰爭、動作等品類大多都有槍械射擊內容,因此,“玩槍”便成了很大一部分遊戲中常見的玩法。當前比較熱門的使命召喚系列、戰地系列、無主之地系列、戰爭機器系列還有最近流行的吃雞類遊戲(絕地求生、堡壘之夜)等等都是玩槍,核心玩法型別上都是屬於射擊類遊戲。因此做好武器與射擊體驗,還原逼真的射擊情景,便成了這類遊戲的核心賣點。

射擊遊戲的分類

  目前的射擊遊戲主要分為兩種,一種為“第一人稱射擊遊戲”(FPS)與“第三人稱射擊遊戲”(TPS)。

第一人稱

  第一人稱射擊遊戲是以玩家主視角進行的射擊遊戲。玩家不再像別的遊戲型別一樣操縱螢幕中的虛擬人物來進行遊戲,而是身臨其境的主視角,體驗遊戲帶來的視覺衝擊,這就大大增強了遊戲的主動性和真實感。

  如上兩張圖分別是1999年發售的反恐精英( Counter-Strike )與2019年發售的使命召喚:現代戰爭 ( Call of Duty: Modern Warfare )。可以看到,時隔20年,場景與槍械變得更加精細與逼真,介面變得精美與合理,但不變的是,螢幕中間都做有一個“準心”。

第三人稱

  第三人稱射擊遊戲與第一人稱區別在於,螢幕上顯示的主角的視野不同,並且第三人稱中玩家控制的遊戲人物在遊戲螢幕上是可見的,因而第三人稱射擊遊戲加強調更強調動作感。

  如上3張圖都是“幽靈行動:斷點”的遊戲畫面,可以看到,在沒有瞄準時螢幕中央是沒有準心的,這時可以將注意力集中在環境、角色動作、遊戲劇情等等。當開啟瞄準時,就會出現螢幕中央的準心,還可以通過Alt鍵切換第一人稱與第三人稱(第二張圖與第三張圖)。

準心

  通過上述的介紹可以看到,無論是第一人稱還是第三人稱,螢幕中央都會有“準心”以方便玩家瞄準。那為什麼要有準心呢?為什麼有了準心之後,我們在遊戲中就可以瞄準目標呢?

現實中的射擊

  我們在物理課程中瞭解過,如果拋去槍的複雜結構不談,槍的工作原理簡單來講就是,彈頭在槍管中受到推進力後做拋物線運動。

  如果彈頭在運動過程中碰撞到目標,則表示擊中;若未碰撞到目標,則表示未擊中。那麼我們如何瞄準才能擊中目標呢?

  如圖所示,藍色的水平線則是瞄準線,代表瞄準方向;綠色是槍管軸心線,代表槍管的指向方向;紅色則是子彈的飛行軌跡。假定子彈每次從槍口中射出的速度是固定的,空氣阻力也是固定的,子彈受到的重力也是固定的,那麼按照上圖的瞄準方式,射擊命中的“遠交點”也是固定的。換句話說,用這把槍和這個瞄準角度的話,只能擊中距離槍口x米的目標。那麼如果我們射擊同一方向上比x米更近或更遠的目標怎麼辦呢?

  如上圖所示,分別為步槍和狙擊槍的射擊距離調整方法。步槍中有瞄準距離刻度線,調節刻度即可;狙擊槍在瞄準高倍鏡中有刻度線,根據目標的距離,使用對應的瞄準刻度線即可;手槍因為射擊距離短(大部分射擊距離在50米以內),瞄準誤差較小,所以不用調節。

遊戲中的射擊

  而在遊戲中,未開瞄準鏡的情形下,角色的持槍動作往往如上圖所示,將槍固定於肘關節處。為什麼要這樣持槍呢?因為這樣持槍姿勢可以減小在第一人稱人下槍所遮住的視野,提升遊戲體驗,而這樣的持槍動作,在現實中是打不準目標的(因為沒有三點一線的瞄準)。在遊戲裡為了能更加便捷開槍,因此加入了準心的概念。

  有了準心之後,玩家就可以更加方便地瞄準目標,從而獲得更好的射擊體驗。但這種準心“指哪打哪”的遊戲效果是如何實現的呢?

UE4中的子彈彈道

  我剛開始在UE4中實現子彈彈道時,想法很簡答,既然子彈是從槍中射出的,那麼子彈的飛行方向當然就是槍口的朝向。為了使子彈能夠擊中“準心”的位置,我調整了槍在手中的朝向。

問題

  經過反覆測試後發現,無論怎麼調,只能保證在某一固定射擊距離時,彈著點能與準心重合。當射擊距離改變時,子彈就無法擊中準心位置,這是什麼原因呢?

  如上圖所示,假定玩家(攝像機)與槍處在同一水平面,從俯視的角度來看,玩家的準心永遠指向正前方(用紅色線條表示),而槍則在玩家左手或右手,其朝向(用綠色線條表示)與玩家朝向存在夾角Θ。在該夾角下,只有玩家與目標距離y時,彈著點才能剛好在準心上。無論怎麼調整夾角Θ,都只有某一個距離下彈著點剛好落在準心上(因為兩條不平行的直線永遠只有一個交點)。而實際上槍與玩家(攝像頭)並不在同一水平面,在三維座標系下,兩條不平行的直線最多有一個交點(可能沒有交點)。

  那麼該如何解決該問題呢?既然兩條線只有一個交點,那麼能不能根據不同的射擊距離改變槍的朝向,使交點始終在準心瞄準位置呢?

  要想調整槍的朝向,就必須調整槍在玩家動畫的骨骼中的相對位置。而且要根據射擊距離實時調整(玩家準心瞄到的障礙物的距離),這個 運算量會非常大,幾乎是不可能實現的,所以這條路走不通,該怎麼辦呢?

解決方案

  既然槍的方向不方便調整,子彈的方向總可以控制吧!可以在開槍時,先根據玩家(攝像機)的朝向,做射線檢測,確定目標彈著點,然後再控制子彈的飛行方向為槍口飛向彈著點,這不就可以實現無論玩家瞄向哪裡,子彈都可以擊中“準心”的位置的需求嗎。

計算彈著點

  如上藍圖程式碼所示,用攝像機方向(即準心的朝向)做射線檢測,返回擊中的彈著點座標與材質(方便後續播放擊中特效),若未擊中任何物體(例如朝天上開槍),則返回射線檢測的終點,方便客戶端展示彈道。

廣播開槍

  如上藍圖程式碼所示,獲得彈著點座標與材質後進行廣播,在所有客戶端上播放該玩家的開槍動畫(武器特效),並呼叫C++函式,利用GamePlayTask生成彈道。

void AWeapon::GenerateBulletTrack(FVector HitLocation, UPhysicalMaterial* HitMaterial)
{
    // 槍口座標
    FVector MuzzleLocation = Mesh->GetSocketLocation("Muzzle");
    // 向量方向:終點 - 起點
    FVector Direction = HitLocation - MuzzleLocation;
    // 飛行速度
    FVector Speed = Direction.Rotation().Vector() * 10000;
    // 飛行時間:距離 / 速度
    float Time = Direction.Size() / Speed.Size();
    // 擊中點特效
    UParticleSystem* HitFX = nullptr;
    // 擊中點材質
    if (HitMaterial)
    {
        EPhysicalSurface SurfaceType = HitMaterial->SurfaceType;
        // 該槍已設定該材質型別的擊中特效
        if (VarHitFXs.Contains(SurfaceType))
        {
            HitFX = VarHitFXs[SurfaceType];
        }
    }
    // 執行子彈軌跡的GamePlayTask
    UBulletTrackTask * TrackTask = UBulletTrackTask::InitBulletTrack(BulletTrackComponent, MuzzleLocation, Speed, TargetFX, HitFX, Time, this, HitLocation);
    TrackTask->ReadyForActivation();
}

  如上C++程式碼所示,根據擊中點座標,計算出子彈的飛行方向與距離,然後計算出飛行時間,並用這些引數開啟子彈軌跡的GamePlayTask,用於顯示客戶端的子彈軌跡及擊中特效。

子彈彈道

void UBulletTrackTask::TickTask(float DeltaTime)
{
    // 記錄飛行時間
    ActiveTime += DeltaTime;
    UWorld* World = GetWorld();
    check(World);
    const FVector OldLocation = CurrentLocation;
    // 勻速距離公式:距離 = 速度 * 時間
    FVector MoveDistance = CurrentVelocity * DeltaTime;
    // 新位置
    CurrentLocation = OldLocation + MoveDistance;
    // 更新軌跡特效的位置與朝向
    if (TrackComponent)
    {
         TrackComponent->SetWorldLocationAndRotation(CurrentLocation, UKismetMathLibrary::MakeRotFromX(CurrentVelocity.GetSafeNormal()));
    }
    // 超過飛行時間
    if (ActiveTime >= LifeTime)
    {
        // 有擊中特效
        if (HitFX)
        {
            // 顯示擊中特效
            UGameplayStatics::SpawnEmitterAtLocation(World, HitFX, HitLocation);
        }
        EndTask();
    }
}

  通過執行該子彈軌跡的GamePlayTask,每幀會更新子彈軌跡特效的位置,顯示子彈的飛行過程,如果超過了子彈的飛行時間,則在伺服器已計算出的擊中點產生擊中特效(受傷害特效)等等,實現了子彈總是能擊中游戲準心瞄準的目標。

展望

  上述解決方案是彈道實現的較簡化版本,因為沒有考慮子彈的飛行時間、子彈重力、槍械後坐力、空氣阻力等諸多因素。如果考慮這些因素的話,伺服器就不適合直接用射線檢測來計算彈著點,而是同樣改用GamePlayTask來實現,以便能夠精確計運算元彈飛行過程中的受力、飛行碰撞等等問題。但這樣做勢必會增加伺服器中彈著點計算耗時(因為會增加子彈飛行時間),加大客戶端的表現困難(讓玩家感受到開槍有延時),這也是該方案後續的難點