안녕하세요.

<가디언즈 오브 레스토레이션>을 1인 개발하고 있는 창작고래 입니다.


올해 정식 출시를 목표로 열심히 작업하고 있습니다.


최근에 스팀의 업적과 스팀덱 진동을 구현을 적용하였습니다.

스팀 업적 부분은 쉽게 적용되었지만 스팀덱 진동에서 많은 시행착오를 격은 거 같습니다.


부족한 스크립트이지만 스팀덱 진동을 구현하시는 분들께 도움이 되었으면 하여 작성한 스크립트를 공유해 드립니다.


UniTask 와 Steamworks .net(ver 2024.8.0)을 사용하였습니다.


스팀덱의 진동의 세기가 6dB이 기본으로 적용되어 있는데 설정에서 12dB로 변경하면 진동이 약하게 체감됩니다.


스팀덱 관련 진동만 스팀 input api를 사용하였습니다.

================================================================================================


using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.Events;

using Cysharp.Threading.Tasks;


#if USE_STEAM

using Steamworks;

#endif


public class SteamSDKManager : MonoBehaviour

{

#if USE_STEAM

    public static SteamSDKManager Instance;

    // 스팀 덱 여부

    public static bool IsSteamDeck;


    private void Awake()

    {

        if (Instance == null) Instance = this;

    }


    bool isInitiallized = false;

    // 실패 시 종료 창 관련 액션

    UnityAction<string> messageAction;

    // 스팀덱 동작 확인 시 스팀덱 핸들 저장용

    InputHandle_t inputHandle;


    /// <summary>

    /// 스팀 초기화 진행

    /// </summary>

    /// <param name="action">실패 시 메시지 창 내용 전달 액션</param>

    /// <returns></returns>

    public async UniTask Initialize(UnityAction<string> action)

    {

        messageAction = action;


        //스팀 클라이언트 실행 여부 확인

        if (!SteamAPI.IsSteamRunning())

        {

            // 초기화를 실패하여 게임을 종료합니다. {0}s

            FailGameQuit(Localize.GetLocalizedString(Const.LOCALIZE_COMMON_SYSTEM,

                "Start_Init_Fail")).Forget();            

            return;

        }


        //스팀 API 초기화 진행

        if (!SteamAPI.Init())

        {

            // 초기화를 실패하여 게임을 종료합니다. {0}s

            FailGameQuit(Localize.GetLocalizedString(Const.LOCALIZE_COMMON_SYSTEM,

                "Start_Init_Fail")).Forget();            

            return;

        }


        // 스팀 API 초기화 성공

        isInitiallized = true;


        // 현재 게임이 사용자의 계정에 있는지 확인

        bool isPurchased = SteamApps.BIsSubscribed();


        if (!isPurchased)

        {

            // 구매 확인 실패로 게임 종료

            FailGameQuit(Localize.GetLocalizedString(Const.LOCALIZE_COMMON_SYSTEM,

                "Purchased_Fail")).Forget();            

            return;

        }


        // 스팀 Input API 초기화 진행

        SteamInput.Init(false);

        // 스팀 Input 동작 체크

        SteamInput.RunFrame();


        // 스팀덱 동작 여부 체크

        IsSteamDeck = SteamUtils.IsSteamRunningOnSteamDeck();


        // 스팀덱 동작일 경우 스팀덱 핸들 얻기

        if (IsSteamDeck)

        {

            StartCoroutine(CoSteamInputCheck());


            

            InputHandle_t[] controllers = new InputHandle_t[Constants.STEAM_INPUT_MAX_COUNT];

            SteamInput.GetConnectedControllers(controllers);


            foreach (var handle in controllers)

            {

                ESteamInputType temp = SteamInput.GetInputTypeForHandle(handle);                

                if (temp == ESteamInputType.k_ESteamInputType_SteamDeckController)

                {

                    inputHandle = handle;

                    break;

                }

            }

        }


        // 임의로 대기 0.3초 넣어줌

        await UniTask.WaitForSeconds(0.3f);

    }


    // 매 프레임 스팀 Input 체크

    IEnumerator CoSteamInputCheck()

    {

        while (true)

        {

            // 중요! 스팀 Input 동작 체크

            SteamInput.RunFrame();

            yield return null;            

        }

    }


    // 실패 시 3초 후 게임 종료 관련

    async UniTask FailGameQuit(string message)

    {

        for (int i = 3; i > 0; i--)

        {            

            messageAction(string.Format(message, i));

            await UniTask.WaitForSeconds(1);

        }

        Application.Quit();

    }


    bool isSetAchievement; //스팀 업적 변경 여부

    bool isUpdateAchievement; //스팀 업적 변경 내용 저장 여부


    /// <summary>

    /// 스팀 업적 달성 등록

    /// </summary>

    /// <param name="key">업적 관련 키</param>

    /// <returns>등록 성공 여부</returns>

    public bool SetAchievement(string key)

    {

        isSetAchievement = false;

        isUpdateAchievement = false;

        if (!isInitiallized) return false;


        if (SteamUserStats.SetAchievement(key)) isSetAchievement = true;

        else isSetAchievement = false;


        isUpdateAchievement = SteamUserStats.StoreStats();

        return isSetAchievement && isUpdateAchievement;

    }


    /// <summary>

    /// 스팀 업적 달성 확인

    /// </summary>

    /// <param name="key">업적 관련 키</param>

    /// <returns>달성 여부</returns>

    public bool CheckAchievement(string key)

    {

        bool result = false;

        if (!isInitiallized) return result;

        bool isSuccess = SteamUserStats.GetAchievement(key, out result);

        if (result && isSuccess) return true;

        else return false;

    }


    /// <summary>

    /// 스팀 업적 초기화

    /// </summary>

    /// <param name="key">업적 관련 키</param>

    public void ClearAchievement(string key)

    {

        if (!isInitiallized) return;

        bool isSuccess = SteamUserStats.ClearAchievement(key);

        if (isSuccess)

        {

            SteamUserStats.StoreStats();

        }

    }


    Coroutine stopRumbleRoutine;

    /// <summary>

    /// 스팀덱 진동 시작

    /// </summary>

    /// <param name="lowFrequency"></param>

    /// <param name="highFrequency"></param>

    /// <param name="duration">지속 시간</param>

    public void SetRumble(float lowFrequency, float highFrequency, float duration)

    {

        if (!isInitiallized) return;

        if (!IsSteamDeck) return;


        // 스팀덱 진동이 워낙 약해서 최대 값 적용하고 강도를 지속시간으로 처리

        SteamInput.TriggerVibration(inputHandle, 65535, 65535);


        if (stopRumbleRoutine != null) StopCoroutine(stopRumbleRoutine);

        stopRumbleRoutine = StartCoroutine(CoSteamDeckStopRumble(inputHandle, duration));

    }


    IEnumerator CoSteamDeckStopRumble(InputHandle_t inputHandle, float duration)

    {

        float timer = 0;

        while (timer < duration)

        {

            timer += Time.unscaledDeltaTime;

            yield return null;

        }


        StopRumble(inputHandle);

        stopRumbleRoutine = null;

    }


    void StopRumble(InputHandle_t inputHandle)

    {

        SteamInput.TriggerVibration(inputHandle, 0, 0);

    }


    // 게임 종료 관련 스팀 API 중단

    private void OnApplicationQuit()

    {

        if (isInitiallized)

        {

            if (stopRumbleRoutine != null)

            {

                StopCoroutine(stopRumbleRoutine);

                if(inputHandle != null) StopRumble(inputHandle);

            }

            SteamAPI.Shutdown();

            isInitiallized = false;

        }

    }


#endif

}



================================================================================================

SteamInput.RunFrame(); 이 부분을 시작 부분과 매프레임에 적용 안하면

IsSteamDeck = SteamUtils.IsSteamRunningOnSteamDeck();

이 부분에서 스팀덱 가동 여부는 true가 되지만 


foreach (var handle in controllers)

            {

                ESteamInputType temp = SteamInput.GetInputTypeForHandle(handle);                

                if (temp == ESteamInputType.k_ESteamInputType_SteamDeckController)

                {

                    inputHandle = handle;

                    break;

                }

            }

이 부분에서 ESteamInputType.k_ESteamInputType_SteamDeckController을 찾지 못하는 문제가 발생하는거 같았습니다.


마지막으로 현재까지 적용된 게임의 0.8.0.0 버전의 트레일러 영상입니다.



감사합니다~ ^^