using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
namespace CloudCode.Modules;
// 구글 응답기
internal class GoogleApiRequest
{
public Document document { get; set; }
public string encodingType { get; set; }
}
internal class Document
{
public string type { get; set; }
public string content { get; set; }
public string languageCode { get; set; }
}
public class SentimentResponse
{
public DocumentSentiment documentSentiment { get; set; }
}
public class DocumentSentiment
{
public float score { get; set; }
public float magnitude { get; set; }
}
public class ModuleConfig : ICloudCodeSetup
{
public void Setup(ICloudCodeConfig config)
{
config.Dependencies.AddSingleton(GameApiClient.Create());
}
}
public class SentimentModule
{
private readonly ILogger<SentimentModule> _logger;
public SentimentModule(ILogger<SentimentModule> logger)
{
_logger = logger;
}
[CloudCodeFunction("AnalyzeSentiment")]
public async Task<float> AnalyzeSentiment(
IExecutionContext context,
IGameApiClient secrets,
string text,
string language)
{
var apiKey = await secrets.SecretManager.GetSecret(context, "GOOGLE_API_KEY");
if(apiKey == null || String.IsNullOrEmpty(apiKey.Value))
{
_logger.LogError("Google API key not found in secrets.");
throw new Exception("Google API key not found in secrets.");
}
var apiUrl = $"https://language.googleapis.com/v2/documents:analyzeSentiment?key={apiKey.Value}";
var requestData = new GoogleApiRequest
{
encodingType = "UTF8",
document = new Document
{
type = "PLAIN_TEXT",
content = text,
languageCode = language
}
};
var jsonData = JsonSerializer.Serialize(requestData);
var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
using var httpClient = new HttpClient();
var response = await httpClient.PostAsync(apiUrl, content);
var responseJson = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_logger.LogError($"Google API request failed {responseJson}");
throw new Exception($"Google API request failed: {responseJson}");
}
_logger.LogInformation("Google API request succeeded.");
var sentimentResponse = JsonSerializer.Deserialize<SentimentResponse>(responseJson);
if(sentimentResponse == null)
{
_logger.LogError("Failed to deserialize Google API response.");
throw new Exception("Failed to deserialize Google API response.");
}
float sentimentScore = sentimentResponse.documentSentiment.score;
return sentimentScore;
}
}
API키는 항상 외부에 드러나면 안되며 클라이언트 로직에서는 이 코드를 알면 안된다 이에따라 API키를 숨기면서 이를 클라에서 절대로 알 수 없게 하는 방법은 서버에 코드를 실행하는 것인데 이를 간단하게 만들어 주는 방법은 Unity Service Game 의 Cloud Code service를 사용하는 것이다
이 서비스는 2가지 방식으로 만들 수 있는데
1. c# 방식 : CLI를 사용하거나 유니티 툴 상의 Deploy패키지를 다운 받는다
2. javascript 방식 : Dashboard 내에서 직접 생성이 가능하다
이 중에 c#방식의 CLI 방식으로 Cloud Code를 서버에 올려 놓는 방식을 선택하였다 -> 그냥 궁금했음
1. UGS 프로젝트 연결
- 유니티 PackageManager에서 Cloud Code패키지를 다운 받는다 -> 이때 주의점은 버전에 따라서 에러가 발생하는 경우가 있을 수 있으니 자신의 유니티 버전에 맞는 것으로 다운그레이드 해야 될 수도 있다
- Edit -> ProjectSetting -> Service 쪽에서 현재 프로젝트를 연동한다
자 이제 프로젝트에 UGS 서비스를 사용할 준비는 되어있다
2. SecretKey 설정
- Add project secert을 통해 자신의 API를 올려둔다

3. Cloud Code 설정
이제 클로드 코드의 C# module쪽으로 보면 아래가 아닌 CLI ~~ 라고 나와 있는데 이 Dashboard에서는 직접 추가가 불가능 하다 (왜 일까??)

- cmd 창을 연다
- npm install -g ugs 입력
- ugs config set project-id <your-project-id> -> project의 setting에 존재한다
- ugs config set environment-name <your-environment-name> -> project의 environment 의 id가 아닌 name이 맞다
- ugs login 을 한다 이때 추가적으로 아래 설정을 해 줘야 된다

이 부분을 생성해 줘야된다

이 역할들을 설정해 줘야 되는데 이를 설정해 주지 않으면 오류가 난다
자 이제 login 까지 했으면 작업을 실행할 c# 코드를 작성해 보자
4. c# Code
- 클래스 라이브러리를 만들어 주자

- Unity.Services.CloudCode.Apis;
- Unity.Services.CloudCode.Core;
- 위 두 서비스가 필요하다 NGet에서 라이브러리를 다운 받자
- 아래와 같이 코드를 작성해 줬는데 하나하나 주석을 달아서 설명해 두었다
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
namespace CloudCode.Modules;
/// <summary>
/// 감성 분석을 위한 클래스 구조
/// JsonSerializer를 사용하기위하여 property로 만들어 줘야된다
/// </summary>
internal class GoogleApiRequest
{
public Document document { get; set; }
public string encodingType { get; set; }
}
internal class Document
{
public string type { get; set; }
public string content { get; set; }
public string languageCode { get; set; }
}
public class SentimentResponse
{
public DocumentSentiment documentSentiment { get; set; }
}
public class DocumentSentiment
{
public float score { get; set; }
public float magnitude { get; set; }
}
/// <summary>
/// DI를 위한 클래스 리플랙션을 이용해 자동으로 GameApiClient를 주입해준다고 한다
/// </summary>
public class ModuleConfig : ICloudCodeSetup
{
public void Setup(ICloudCodeConfig config)
{
config.Dependencies.AddSingleton(GameApiClient.Create());
}
}
// 감성분석 모듈
public class SentimentModule
{
// logs 창에서 로그를 확인하기 위한 로거이다 -> DI주입
private readonly ILogger<SentimentModule> _logger;
public SentimentModule(ILogger<SentimentModule> logger)
{
_logger = logger;
}
// 아래 모듈이 실제로 실행 시킬수 있는 함수이다
// IExcutionContext과 IGameApiClient는 DI주입
// text와 language는 클라이언트에서 전달해주는 파라미터
[CloudCodeFunction("AnalyzeSentiment")]
public async Task<float> AnalyzeSentiment(
IExecutionContext context,
IGameApiClient secrets,
string text,
string language)
{
var apiKey = await secrets.SecretManager.GetSecret(context, "GOOGLE_API_KEY");
if(apiKey == null || String.IsNullOrEmpty(apiKey.Value))
{
_logger.LogError("Google API key not found in secrets.");
throw new Exception("Google API key not found in secrets.");
}
var apiUrl = $"https://language.googleapis.com/v2/documents:analyzeSentiment?key={apiKey.Value}";
var requestData = new GoogleApiRequest
{
encodingType = "UTF8",
document = new Document
{
type = "PLAIN_TEXT",
content = text,
languageCode = language
}
};
var jsonData = JsonSerializer.Serialize(requestData);
var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
using var httpClient = new HttpClient();
var response = await httpClient.PostAsync(apiUrl, content);
var responseJson = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_logger.LogError($"Google API request failed {responseJson}");
throw new Exception($"Google API request failed: {responseJson}");
}
_logger.LogInformation("Google API request succeeded.");
var sentimentResponse = JsonSerializer.Deserialize<SentimentResponse>(responseJson);
if(sentimentResponse == null)
{
_logger.LogError("Failed to deserialize Google API response.");
throw new Exception("Failed to deserialize Google API response.");
}
float sentimentScore = sentimentResponse.documentSentiment.score;
return sentimentScore;
}
}
- 이렇게 만들었으면 이를 게시해 줘야된다



이런식으로 로컬에 작성해 두고 게시해 준다
- 게시한 폴더에 들어가 보면 아래 코드들이 있는데 ccm은 없을 것이다

- 폴더에 있는 모든 코드를 zip으로 압축한뒤 Language.dll의 이름에 맞춰서 이름을 설정하고 ccm확장자로 변경해 준다
- cmd 창을 다시 들어간 다음 ugs deploy <ccm파일경로> 를 입력한다
- 성공적으로 되었으면 Dashboard에 이가 나타났을거고 오류가 났으면 세팅이 잘못되었거나 role을 설정해주지 않았을 거다
5. unity 에서 호출
아래 코드를 분석하면 간단하게 알 수 있을 것이다 return 값이 다른건 내 실제 게임은 CloudCode의 Task<string>으로 return이 되어있기 때문이다
public class SentimentAnalysisManager : MonoBehaviour
{
public string TestText = "";
// 예제 테스트용
async void Start()
{
// 씬이 시작될 때 "I love this game!" 텍스트 분석 시도
await AnalyzeText("I love this game!", "en");
// 씬이 시작될 때 "이 게임 정말 싫어." 텍스트 분석 시도
await AnalyzeText(TestText, "ko");
}
// 테스트용 UI에서 호출할 수 있도록 public으로 선언
public async Task AnalyzeText(string textToAnalyze, string languageCode)
{
try
{
if (UnityServices.State != ServicesInitializationState.Initialized)
{
await InitializeAndSignInAsync();
}
Debug.Log($"분석 시작 '{textToAnalyze}'");
var args = new Dictionary<string, object>
{
{ "text", textToAnalyze },
{ "language", languageCode }
};
string responseJson = await CloudCodeService.Instance.CallModuleEndpointAsync<string>(
"Language",
"AnalyzeSentiment",
args
);
if (string.IsNullOrEmpty(responseJson))
{
Debug.LogWarning("어딘가 이상하니 서버를 확인해 보자");
}
else
{
Debug.Log($"데이터 얻기 성공");
SentimentResponse responseData = JsonUtility.FromJson<SentimentResponse>(responseJson);
Debug.Log($"Sentiment Score: {responseData.documentSentiment.score}, Magnitude: {responseData.documentSentiment.magnitude}");
}
}
catch (Exception e)
{
Debug.LogError($"분석 실패: {e.Message}");
}
}
private async Task InitializeAndSignInAsync()
{
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
Debug.Log("익명 로그인");
}
}
}
6. 추가정보
- 비용감소는 문서를 뒤져보면서 파악하자 https://docs.unity.com/ugs/en-us/manual/cloud-code/manual/modules/reference/cost
- 위를 자동화 하고싶다면 CI/CD를 사용하자 https://docs.unity.com/ugs/ko-kr/manual/cloud-code/manual/modules/how-to-guides/automation
- 귀찮으면 AI한테 JavaScript로 짜달라고 하거나 유니티 deploy툴을 활용하자 -> 설명이 잘 되어있다