最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

Unity程序猿勇闖茶杯之魂(一)

2018-09-17 00:23 作者:皮皮關(guān)做游戲  | 我要投稿

作者:Yukine


本工程難度:★★☆☆☆

前言

《茶杯頭》(Cuphead)這個(gè)游戲,獲得TGA 2017最佳藝術(shù)指導(dǎo)和最佳獨(dú)立游戲兩項(xiàng)大獎(jiǎng),可謂實(shí)至名歸。

記得去年6月的時(shí)候我還在苦苦等待某狗頭人的黑暗劍21(已經(jīng)不存在了),在看E3發(fā)布會(huì)直播時(shí),一款名為茶杯頭的游戲立刻吸引了我,就沖這復(fù)古的畫(huà)風(fēng)我肯定買(mǎi)爆?。〈桨l(fā)售日聽(tīng)聞好評(píng)一片而且難度極高,我心里暗自琢磨:哼,我可是黑魂三部曲總時(shí)間超過(guò)兩百個(gè)小時(shí),和歷代薪王談笑風(fēng)生,什么大場(chǎng)面沒(méi)見(jiàn)過(guò),一個(gè)小小的茶杯頭還想難倒我?拿起手柄開(kāi)搞!然后我就被打臉了……

看到最多的就是這個(gè)畫(huà)面

經(jīng)歷了重重磨難通關(guān)后,回頭看了眼自己的死亡記錄,emmmmm……

大概就是這樣吧……

似乎找到了當(dāng)初激情黑魂的感覺(jué),茶杯之魂,名不虛傳,在下佩服。

嘛,總之茶杯頭是一款素質(zhì)極佳的橫板闖關(guān)游戲,在剛發(fā)售的那段時(shí)間里成為了各大游戲主播的新受苦素材,人氣頗高。實(shí)際上,這款游戲就是用Unity制作的,這就引起了我的興趣,作為一名Unity程序猿,肯定要嘗試復(fù)刻一下啦。那么接下來(lái),就開(kāi)始我們的Cuphead之旅吧。本期先從主角茶杯頭的基本操作開(kāi)始介紹。


狀態(tài)分析

對(duì)于角色的基本操作來(lái)說(shuō),如何處理好角色狀態(tài)之間的切換是重中之重。茶杯頭的教學(xué)關(guān)卡中幾乎已經(jīng)囊括了角色的所有狀態(tài),基本可以總結(jié)為下圖中的幾大類(lèi)。在本期中僅介紹其中的一部分供大家參考。主要運(yùn)用到的Unity模塊為動(dòng)畫(huà)模塊和2D物理模塊。

角色狀態(tài)一覽

1.角色移動(dòng)

首先角色的默認(rèn)狀態(tài)為站立閑置,當(dāng)按下方向鍵(A或D)時(shí)向左/右移動(dòng),抬起時(shí)停止移動(dòng)。同時(shí)需要注意,在處于下蹲狀態(tài)時(shí)角色是不會(huì)移動(dòng)的,所以在移動(dòng)的函數(shù)中要先判斷是否處于下蹲狀態(tài)再根據(jù)情況改變角色速度。

private void _Move(int dic) //dic代表面向方向,左為-1,右為1

{

    if (!_isDown) //判斷是否為下蹲狀態(tài)

    {

        _rigidbody.velocity = new Vector2(dic * _moveSpeed, _rigidbody.velocity.y); //直接用rigidbody.velocity賦予角色速度

        _animator.SetBool("IsWalk", true); //觸發(fā)行走動(dòng)畫(huà)

        _isWalking = true;

    }

   

    //當(dāng)前進(jìn)方向與面向方向不一致時(shí)反轉(zhuǎn)角色

    if (dic > 0 && !_isFacingRight)

    {

        _Reverse();

    }

    else if (dic < 0 && _isFacingRight)

    {

        _Reverse();

    }

}

 

private void _StopMove() //停止移動(dòng)

{

    //判斷是否為沖刺狀態(tài),若在沖刺則保持速度不變

    if (!_isDash)

    {

        _rigidbody.velocity = new Vector2(0, _rigidbody.velocity.y);

    }

    else

    {

        _rigidbody.velocity = new Vector2(_rigidbody.velocity.x, _rigidbody.velocity.y);

    }

 

    _animator.SetBool("IsWalk", false);

    _isWalking = false;

}

 


角色移動(dòng)效果如下圖(一個(gè)完整的左腳換右腳再換回左腳總共是16張圖片組成的幀動(dòng)畫(huà),摳圖摳的爽歪歪):


2.角色跳躍

角色跳躍直接改變角色在Y軸方向上的速度,這里的重點(diǎn)在于站立動(dòng)畫(huà)和跳躍動(dòng)畫(huà)的切換。我在這里采用的方法是在角色腳下放置監(jiān)測(cè)點(diǎn),通過(guò)向下發(fā)射射線檢測(cè)的方式確認(rèn)角色是否接觸地面并控制動(dòng)畫(huà)狀態(tài)機(jī)的相關(guān)變量,射線檢測(cè)的距離可以按照實(shí)際需要調(diào)整。

//Update函數(shù)中的相關(guān)代碼

if (!_isGrounded && !_isJump)

{

    _animator.SetBool("IsJump", true);

    _isJump = true;

}

else if(_isGrounded)

{

    _animator.SetBool("IsJump", false);

    _isJump = false;

 

    if (!_isAlreadyLand)

    {

        GroundDust(); //產(chǎn)生落地的灰塵

        _isAlreadyLand = true;

    }

}

//End Update

 

private void _Jump() //角色跳躍

{

    _rigidbody.velocity = new Vector2(_rigidbody.velocity.x, _jumpSpeed);

}

 

跳躍效果如下(特地看了一下原版游戲,一次完整的跳躍大概是跳躍動(dòng)畫(huà)循環(huán)兩次,哇,我是有多無(wú)聊……):


3.角色下蹲

下蹲也是檢測(cè)按鈕狀態(tài),按下或按住則蹲下,抬起則恢復(fù)到站立狀態(tài)。這里的關(guān)鍵點(diǎn)在于如果處于跳躍狀態(tài)一定要將下蹲狀態(tài)關(guān)閉,否則在下蹲時(shí)按下跳躍按鈕并在跳躍狀態(tài)中放開(kāi)下蹲按鈕,當(dāng)角色落地時(shí)就會(huì)一直處于下蹲狀態(tài)(嗯,bug就是這么產(chǎn)生的……)

private void _Down(bool isDown)

{

    _animator.SetBool("IsDown", isDown);

    _isDown = isDown;

 

    if(_isJump) //跳躍時(shí)下蹲無(wú)效

    {

        return;

    }

 

    if(isDown) //下蹲時(shí)停止移動(dòng)

    {

        _StopMove();

    }

}

 

效果如下(看我上下鬼畜?。?/p>


4.角色沖刺

角色沖刺時(shí)要注意是不受重力影響的,所以在角色處于沖刺狀態(tài)時(shí)將其重力影響設(shè)置為0,沖刺結(jié)束后再設(shè)置回來(lái);并且還要考慮在空中的特殊情況,如果在空中已經(jīng)沖刺則需要禁止再次觸發(fā)沖刺狀態(tài),以免出現(xiàn)在空中多次沖刺的尷尬場(chǎng)面……

//Update中的相關(guān)代碼

if(_isDash && (_stateInfo.IsName("DashGround") || _stateInfo.IsName("DashAir")))

{

    _rigidbody.velocity = new Vector2(_faceDic * _dashSpeed, 0);

 

    if(_stateInfo.normalizedTime > 0.9)

    {

        _rigidbody.velocity = new Vector2(0, 0);

        _rigidbody.gravityScale = 6;

        _isDash = false;

 

        _animator.SetTrigger("QuitDash");      

    }

}

//End Update

 

private void _Dash()

{

    if(_isAlreadyAirDash) //如果在空中已經(jīng)沖刺則返回

    {

        return;

    }

 

    if(!_isGrounded) //在地面和空中分別觸發(fā)不同的動(dòng)畫(huà)

    {

        _rigidbody.gravityScale = 0;

        _animator.SetTrigger("AirDash");

        _isAirDash = true;

 

        _isAlreadyAirDash = true;

    }

    else

    {

        _animator.SetTrigger("GroundDash");

    }

 

    _isDash = true;

}

 

效果如下(請(qǐng)自行腦補(bǔ)xiu~xiu~xiu~的聲音):


5.角色射擊

射擊狀態(tài)是條件限制最多的狀態(tài),需要考慮的情況較多。首先,原地站立時(shí)按下上鍵會(huì)向上射擊。行走時(shí)按下射擊鍵會(huì)切換到行走射擊動(dòng)畫(huà),這里需要運(yùn)用到blend tree,如果僅按下射擊鍵則將Thresh值設(shè)置到向正前方射擊的區(qū)域;如果還按下了上鍵,需要將Thresh值設(shè)置到斜上方射擊的區(qū)域;如果放開(kāi)射擊鍵,還需要將Thresh值設(shè)置到初始未射擊的區(qū)域。如果處于跳躍狀態(tài),僅可向正前方和下方發(fā)射子彈。好吧,說(shuō)的我自己都暈了,但這里一定要注意這些細(xì)節(jié)的處理。

private void _Shoot()

{

    _shootDic = new Vector2(_faceDic, 0); //射擊方向,為后面的子彈運(yùn)動(dòng)做準(zhǔn)備

    _bulletBornPos = _standBulletPos; //子彈產(chǎn)生點(diǎn)

 

    if (_isWalking && !_isJump) //在地面行走時(shí)射擊

    {

        if(Input.GetKeyDown(KeyCode.W) || Input.GetKey(KeyCode.W)) //若按下向上按鈕,動(dòng)畫(huà)變?yōu)橄蛐鄙戏缴鋼舻耐瑫r(shí)步行

        {

            _animator.SetFloat("WalkState", 1f);

            _shootDic = new Vector2(_faceDic, 1).normalized;

            _bulletBornPos = _walkRightUpBulletPos;

        }

        else

        {

            _animator.SetFloat("WalkState", 0.333f);

            _bulletBornPos = _walkRightBulletPos;

        }

 

        _timeCount += Time.deltaTime;

 

        if (_timeCount >= 0.0165 * 9) //設(shè)定行走射擊的時(shí)間間隔

        {

            _timeCount = 0f;

            WalkShoot();

        }

    }

    else if(!_isWalking && !_isJump) //原地站立時(shí)射擊

    {

        _animator.SetBool("IsShoot", true);

 

        if (Input.GetKeyDown(KeyCode.W) || Input.GetKey(KeyCode.W)) //若按下向上按鈕則播放向上射擊動(dòng)畫(huà)

        {

            _animator.SetFloat("IdleState", 1f);

            _shootDic = new Vector2(0, 1).normalized;

            _bulletBornPos = _standUpBulletPos;

        }

        else

        {

            _animator.SetFloat("IdleState", 0f);

            _bulletBornPos = _standBulletPos;

        }

    }

    //蹲下及跳躍時(shí)射擊請(qǐng)參考源代碼

    _isShoot = true;

}

 

運(yùn)行效果如下(吃我一發(fā)空氣彈?。?/p>

讓子彈飛

角色的基本操作暫時(shí)先介紹這么多,接下來(lái)要介紹子彈運(yùn)動(dòng)及動(dòng)畫(huà)效果該如何實(shí)現(xiàn)。

首先要做的,是要確定子彈的產(chǎn)生位置,為此,需要在角色身上按照不同的射擊動(dòng)畫(huà)時(shí)手指的位置設(shè)置好子彈誕生點(diǎn):

角色身上掛載的子彈產(chǎn)生點(diǎn)

接下來(lái)就是該在何時(shí)產(chǎn)生子彈,如果是站立或下蹲時(shí)射擊,則在射擊動(dòng)畫(huà)的第一幀添加發(fā)射子彈的幀事件;若是在行走或跳躍時(shí)射擊,則按照一定的時(shí)間間隔產(chǎn)生子彈。同時(shí)在玩家的相關(guān)代碼中要將射擊子彈的消息傳遞給子彈管理器,這里我采用的是事件傳遞的方式:

public event Action<Vector2, Vector2> OnShoot; //站立射擊實(shí)踐

public event Action<Vector2, Vector2, Transform> OnWalkShoot; //行走射擊事件

 

public void ShootBullet() //站立射擊的幀事件

{

    Vector2 bornPos = _bulletBornPos.position;

 

    if (OnShoot != null)

    {

        OnShoot(bornPos, _shootDic);

    }

}

 

//行走射擊時(shí)需要再傳一個(gè)子彈產(chǎn)生點(diǎn)的transform參數(shù),其余和站立射擊相似

 

我們還需要一個(gè)子彈管理器來(lái)生成子彈,將玩家的射擊事件傳入并在特定位置創(chuàng)建子彈,同時(shí),我們也可以在這個(gè)腳本中管理子彈生成時(shí)的特效。這里需要注意的是在行走時(shí)射擊的話,需要將子彈生成特效的父節(jié)點(diǎn)設(shè)置為子彈生成點(diǎn),這樣的話,biubiubiu特效就能跟著手指走啦;還需要特別指出一點(diǎn),向正前方射擊時(shí)每一顆子彈并不是在同一水平面上的,也就是說(shuō),子彈要有一定的垂直偏移,這樣才有一種子彈忽上忽下的視覺(jué)效果,在代碼中就需要一個(gè)列表來(lái)存儲(chǔ)子彈的垂直偏移量,通過(guò)循環(huán)獲取列表的方式改變子彈位置的Y值:


private void _OnShoot(Vector2 bornPos, Vector2 shootDic) //站立射擊

{

    var born = _GetBornInstance();

    born.transform.position = new Vector3(bornPos.x, bornPos.y, -1);

 

    var bullet = _GetBulletInstance();

    bullet.transform.position = _GetOffsetPos(bornPos);

    bullet.GetComponent<Bullet>().StartMove(shootDic);

}

 

//行走射擊時(shí)需要設(shè)置子彈生成特效的父節(jié)點(diǎn),其余和站立射擊相似

 

private Vector3 _GetOffsetPos(Vector2 pos) //獲取子彈位置的偏移量

{

    var offsetY = pos.y + _shootOffsets[_curOffsetIndex];

 

    if(_curOffsetIndex < 2)

    {

        _curOffsetIndex += 1;

    }

    else

    {

        _curOffsetIndex = 0;

    }

 

    return new Vector3(pos.x, offsetY, -1);

}

 

子彈的運(yùn)動(dòng)直接通過(guò)改變transform.position實(shí)現(xiàn),這里的重點(diǎn)之一是要將子彈旋轉(zhuǎn)至前進(jìn)方向,所以在開(kāi)始運(yùn)動(dòng)前要先運(yùn)用四元旋轉(zhuǎn)改變子彈的rotation值;第二點(diǎn)要注意的是,如果子彈接觸到地面或者敵人要有對(duì)應(yīng)的反饋(也就是擊中時(shí)的幀動(dòng)畫(huà)),這里我直接在子彈的預(yù)制體上加了trigger,運(yùn)用trigger enter來(lái)判斷是否要觸發(fā)相應(yīng)動(dòng)畫(huà):


public void StartMove(Vector2 shootDir) //開(kāi)始運(yùn)動(dòng)

{

    _isMoving = true;

    _moveDir = new Vector3(shootDir.x, shootDir.y, 0).normalized;

    var origin = new Vector3(1, 0, 0).normalized;

    var rotate = Quaternion.FromToRotation(origin, _moveDir); //根據(jù)子彈前進(jìn)方向?qū)ζ湫D(zhuǎn)

    this.transform.rotation = rotate;

}

 

private void _Destroy() //子彈的銷(xiāo)毀

{

    Destroy(this.gameObject);

    _isMoving = false;

}

 

private void OnTriggerEnter2D(Collider2D collision)

{

    if (collision.gameObject.layer == _groundLayer.value) //子彈觸碰到地面時(shí)觸發(fā)子彈擊中動(dòng)畫(huà)

    {

        _isMoving = false;

        _animator.SetTrigger("Hit");

    }

}

 

其實(shí),這種直接銷(xiāo)毀子彈的方法消耗很大,若想獲得更好的運(yùn)行效率,需要運(yùn)用到對(duì)象池,關(guān)于對(duì)象池,大家可以參考這篇文章【Unity】工具類(lèi)系列教程——對(duì)象池!


一切準(zhǔn)備就緒,那么就讓子彈飛起來(lái)吧:


And More

在茶杯頭中,角色行走時(shí),沖刺時(shí),落地時(shí)均會(huì)產(chǎn)生灰塵濺散的效果,滿滿的都是細(xì)節(jié)。那么為了表現(xiàn)這些效果,我們同樣要先確定好灰塵生成的位置并在玩家腳本中運(yùn)用事件觸發(fā)。接著和子彈一樣再創(chuàng)建一個(gè)灰塵管理器,這里也有個(gè)細(xì)節(jié)需要注意,游戲中行走時(shí)的灰塵是有多個(gè)種類(lèi)的(只能佩服制作組的良心),這里我只準(zhǔn)備了三個(gè)預(yù)制體,在每次生成行走灰塵時(shí)要隨機(jī)選取其中的一個(gè):

private GameObject _GetDustInstance(DustType rType) //根據(jù)灰塵的種類(lèi)獲取對(duì)應(yīng)的灰塵預(yù)制體

{

    switch(rType)

    {

        case DustType.WalkDust:

            int index = Random.Range(0, 3); //獲取隨機(jī)數(shù)以便隨機(jī)獲取行走灰塵

            return Instantiate(_dustPrefabList[index]);

        case DustType.DashDust:

            return Instantiate(_dashDustPrefab);

        case DustType.GroundDust:

            return Instantiate(_groundDustPrefab);

    }

    return null;

}

 

private void _CreateDust(Vector2 pos, DustType rType)

{

    var instance = _GetDustInstance(rType);

    instance.transform.position = new Vector3(pos.x, pos.y, -2);

}

 

最終的效果就像下面這樣啦(突然就酷炫了起來(lái)):


那么本期就介紹到這里了,完整代碼請(qǐng)移步:https://github.com/Yukimine33/CupheadCodeByMyself

其實(shí)角色邏輯并不難,但要有足夠的耐心去調(diào)整細(xì)節(jié),還要把大量的精力放在裁剪圖片及制作幀動(dòng)畫(huà)上,可想而知制作組花費(fèi)了多少心血才能給玩家?guī)?lái)這樣一部作品,也希望大家去多多支持這樣的良心作品。下一期會(huì)補(bǔ)全角色的基本操作并開(kāi)始Boss的制作。


想系統(tǒng)學(xué)習(xí)游戲開(kāi)發(fā)的童鞋,歡迎訪問(wèn) http://levelpp.com/     

游戲開(kāi)發(fā)攪基QQ群:869551769     

微信公眾號(hào):皮皮關(guān)

Unity程序猿勇闖茶杯之魂(一)的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
邵阳市| 梅州市| 嘉善县| 河津市| 静安区| 商都县| 阜新市| 阿坝| 波密县| 揭西县| 淮阳县| 富阳市| 白山市| 赤水市| 来安县| 秦皇岛市| 加查县| 沂水县| 阿拉善左旗| 金塔县| 东莞市| 白山市| 张家界市| 法库县| 石阡县| 大连市| 瑞安市| 大理市| 洞头县| 缙云县| 乡城县| 巨鹿县| 信阳市| 达孜县| 新绛县| 固阳县| 上蔡县| 九龙县| 沙雅县| 太仓市| 竹山县|