diff --git a/.gitignore b/.gitignore index 58cbc82..a58b783 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,7 @@ crashlytics-build.properties # Temporary auto-generated Android Assets /[Aa]ssets/[Ss]treamingAssets/aa.meta /[Aa]ssets/[Ss]treamingAssets/aa/* + +# Open AI API Key +/[Aa]ssets/[Rr]esources/ApiKey.txt +/[Aa]ssets/[Rr]esources/ApiKey.txt.meta \ No newline at end of file diff --git a/Assets/Resources.meta b/Assets/Resources.meta new file mode 100644 index 0000000..b1af25e --- /dev/null +++ b/Assets/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 23df1d1da2f798f4ab0460ad4df0658e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 9421266..0856f94 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -202,7 +202,54 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &764219436 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 764219438} + - component: {fileID: 764219437} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &764219437 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 764219436} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 75c1c8d2f998caf46b7f794475bd2e1d, type: 3} + m_Name: + m_EditorClassIdentifier: + PlayerHP: 100 + EnemyHP: 100 +--- !u!4 &764219438 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 764219436} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Assets/Scripts/ChatGPTConnection.cs b/Assets/Scripts/ChatGPTConnection.cs new file mode 100644 index 0000000..85993a5 --- /dev/null +++ b/Assets/Scripts/ChatGPTConnection.cs @@ -0,0 +1,37 @@ +using UnityEngine.Networking; +using Newtonsoft.Json; + +namespace HackathonA +{ + public class ChatGPTConnection + { + private readonly JsonSerializerSettings settings = new(); + private readonly string apiKey; + private readonly string apiUrl; + + + public ChatGPTConnection(string apiKey, string apiUrl) + { + this.apiKey = apiKey; + this.apiUrl = apiUrl; + settings.NullValueHandling = NullValueHandling.Ignore; + } + + public RequestHandler CreateCompletionRequest(ChatGPTDatas.RequestData requestData) + { + var json = JsonConvert.SerializeObject(requestData, settings); + + byte[] data = System.Text.Encoding.UTF8.GetBytes(json); + + var request = new UnityWebRequest(apiUrl, "POST") + { + uploadHandler = new UploadHandlerRaw(data), + downloadHandler = new DownloadHandlerBuffer() + }; + request.SetRequestHeader("Authorization", $"Bearer {this.apiKey}"); + request.SetRequestHeader("Content-Type", "application/json"); + + return new RequestHandler(request); + } + } +} diff --git a/Assets/Scripts/ChatGPTConnection.cs.meta b/Assets/Scripts/ChatGPTConnection.cs.meta new file mode 100644 index 0000000..6364754 --- /dev/null +++ b/Assets/Scripts/ChatGPTConnection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23ca9e3a45225224d9328b7532ec5915 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ChatGPTDatas.cs b/Assets/Scripts/ChatGPTDatas.cs new file mode 100644 index 0000000..65828d3 --- /dev/null +++ b/Assets/Scripts/ChatGPTDatas.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace HackathonA.ChatGPTDatas +{ + [Serializable] + public class RequestData + { + public string model = "gpt-3.5-turbo"; + public List messages; + public float? temperature = null; // [0.0 - 2.0] + public float? top_p = null; + public int? n = null; + public bool? stream = null; + public List stop = null; + public int? max_tokens = null; + public float? presence_penalty = null; + public float? frequency_penalty = null; + public Dictionary logit_bias = null; + public string user = null; + } + + [Serializable] + public class Message + { + public string role; + public string content; + } + + [Serializable] + public class Usage + { + public int prompt_tokens; + public int completion_tokens; + public int total_tokens; + } + + [Serializable] + public class Choice + { + public Message message; + public string finish_reason; + public int index; + } + + [Serializable] + public class ResponseData + { + public string id; + public string @object; + public int created; + public string model; + public Usage usage; + public List choices; + } +} diff --git a/Assets/Scripts/ChatGPTDatas.cs.meta b/Assets/Scripts/ChatGPTDatas.cs.meta new file mode 100644 index 0000000..4865249 --- /dev/null +++ b/Assets/Scripts/ChatGPTDatas.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 587f7d94992790c4e9b6674dd2eb0390 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/DebugLoger.cs b/Assets/Scripts/DebugLoger.cs new file mode 100644 index 0000000..da001c9 --- /dev/null +++ b/Assets/Scripts/DebugLoger.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics; +using Unity.VisualScripting; +using UnityEngine; + +namespace HackathonA +{ + public class DebugLoger + { + [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] + public static void LogError(string error) + { + UnityEngine.Debug.LogError(error); + } + + [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] + public static void Log(string error) + { + UnityEngine.Debug.Log(error); + } + } +} diff --git a/Assets/Scripts/DebugLoger.cs.meta b/Assets/Scripts/DebugLoger.cs.meta new file mode 100644 index 0000000..ef2a83d --- /dev/null +++ b/Assets/Scripts/DebugLoger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6160ddfcc82e00449d6a5431d424bc5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/EnemyAI.cs b/Assets/Scripts/EnemyAI.cs new file mode 100644 index 0000000..569f1fc --- /dev/null +++ b/Assets/Scripts/EnemyAI.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using HackathonA.ChatGPTDatas; + +namespace HackathonA +{ + public class EnemyAI + { + public int DefaultPhysicalAttack { get; set; } = 20; + public int DefaultMagicAttack { get; set; } = 20; + public int DefaultRecoveryPoint { get; set; } = 15; + public int DefaultActionOnError { get; set; } = 0; + + private const string API_URL = "https://api.openai.com/v1/chat/completions"; + private readonly string _apiKey; + private readonly ChatGPTConnection _chatGPTConnection; + + // ChatGPTに入力するメッセージのリスト(過去の内容も一緒に渡すためリスト) + private List _messages; + + public EnemyAI(string apiKey) + { + _apiKey = apiKey; + _messages = new List() + { + new Message(){role = "system", content = GenerateSystemMessage()}, + }; + _chatGPTConnection = new ChatGPTConnection(_apiKey, API_URL); + } + + private string GenerateSystemMessage() + { + return GenerateSystemMessage(DefaultPhysicalAttack, DefaultMagicAttack, DefaultRecoveryPoint); + } + + private string GenerateSystemMessage(int physicalAttack, int mamgicAttack, int recoveryPoint) + { + return @$"Please answer with an integer from 0-4 for your action + +You can take the following actions +0: Physical attack. {physicalAttack} damage +1: Magic attack. {mamgicAttack} damage +2: Recovery. {recoveryPoint} recovery +3: Physical Counter. bounce back if opponen's action is Physical attack +4: Magic counter. bounce back if opponent's action is Magic attack + +Input example: Your HP is currently 1000 and your opponent's HP is 1000 +Output example: 0"; + } + + // 敵の設定 + public void Set(int physicalAttack, int mamgicAttack, int recoveryPoint) + { + _messages = new List() + { + new Message(){role = "system", content = GenerateSystemMessage(physicalAttack, mamgicAttack, recoveryPoint)}, + }; + } + + /// + /// ChatGPTとの通信を開始 + /// + /// プレイヤーのHP + /// 敵のHP + /// アクションのタイプ(0-4)。エラー時にはDefaultActionOnErrorで設定した値が返ってくる + public async UniTask GetEnemyActionAsync(int playerHP, int enemyHP) + { + string currentMessage = $"Currently, your HP is {enemyHP} and your opponent's HP is {playerHP}.\nChoose from the options above and respond with the corresponding number."; + DebugLoger.Log(currentMessage); + _messages.Add(new Message() { role = "user", content = currentMessage }); + + using var request = _chatGPTConnection.CreateCompletionRequest(new RequestData() { messages = _messages }); + + await request.SendAsync(); + + // エラーがあった場合は、それをコンソールに出力 + if (request.IsError) + { + DebugLoger.LogError(request.Error); + return DefaultActionOnError; + } + else + { + var responseMessage = request.Response.choices[0].message.content; + DebugLoger.Log($"ChatGPT replied '{responseMessage}'"); + + // 文字列を数値に変換 + if (int.TryParse(responseMessage[..1], out var action)) + { + if (0 <= action && action <= 4) + { + return action; + } + else + { + return DefaultActionOnError; + } + } + else + { + + DebugLoger.LogError($"Unable to parse '{responseMessage}'"); + return DefaultActionOnError; + } + } + } + } +} diff --git a/Assets/Scripts/EnemyAI.cs.meta b/Assets/Scripts/EnemyAI.cs.meta new file mode 100644 index 0000000..bb998a4 --- /dev/null +++ b/Assets/Scripts/EnemyAI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6692c24e8a5bcd5468c0165516de2d29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/RequestHandler.cs b/Assets/Scripts/RequestHandler.cs new file mode 100644 index 0000000..00f98bc --- /dev/null +++ b/Assets/Scripts/RequestHandler.cs @@ -0,0 +1,44 @@ +using UnityEngine.Networking; +using Newtonsoft.Json; +using Cysharp.Threading.Tasks; +using System; + +namespace HackathonA +{ + public class RequestHandler: IDisposable + { + public bool IsCompleted { get; private set; } + public bool IsError => Error != null; + public string Error { get; private set; } + public ChatGPTDatas.ResponseData Response { get; private set; } + + private UnityWebRequest request; + + public RequestHandler(UnityWebRequest request) + { + this.request = request; + } + + public async UniTask SendAsync() + { + using (request) + { + await request.SendWebRequest(); + + if (request.result != UnityWebRequest.Result.Success) + { + Error = "[ChatGPTConnection] " + request.error + "\n\n" + request.downloadHandler.text; + } + else + { + Response = JsonConvert.DeserializeObject(request.downloadHandler.text); + } + } + } + + public void Dispose() + { + request?.Dispose(); + } + } +} diff --git a/Assets/Scripts/RequestHandler.cs.meta b/Assets/Scripts/RequestHandler.cs.meta new file mode 100644 index 0000000..efbf057 --- /dev/null +++ b/Assets/Scripts/RequestHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27635be83f21f5341abfae620bd21531 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SampleGameBehaviour.cs b/Assets/Scripts/SampleGameBehaviour.cs new file mode 100644 index 0000000..f234139 --- /dev/null +++ b/Assets/Scripts/SampleGameBehaviour.cs @@ -0,0 +1,18 @@ +using UnityEngine; + +namespace HackathonA +{ + public class SampleGameBehaviour : MonoBehaviour + { + public int PlayerHP = 100; + public int EnemyHP = 100; + + async void Start() + { + var config = Resources.Load("ApiKey") as TextAsset; + var _apiKey = config.text.Trim(); + var enemyBehaviour = new EnemyAI(_apiKey); + DebugLoger.Log($"Enemy Action: {await enemyBehaviour.GetEnemyActionAsync(PlayerHP, EnemyHP)}"); + } + } +} diff --git a/Assets/Scripts/SampleGameBehaviour.cs.meta b/Assets/Scripts/SampleGameBehaviour.cs.meta new file mode 100644 index 0000000..e7e4a4a --- /dev/null +++ b/Assets/Scripts/SampleGameBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 75c1c8d2f998caf46b7f794475bd2e1d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 9256c20..6635ea5 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,10 +1,12 @@ { "dependencies": { + "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", "com.unity.collab-proxy": "2.0.3", "com.unity.feature.2d": "1.0.0", "com.unity.ide.rider": "3.0.20", "com.unity.ide.visualstudio": "2.0.18", "com.unity.ide.vscode": "1.2.5", + "com.unity.nuget.newtonsoft-json": "3.2.1", "com.unity.test-framework": "1.1.31", "com.unity.textmeshpro": "3.0.6", "com.unity.timeline": "1.6.4", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index ba7f5b2..0a26664 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,5 +1,12 @@ { "dependencies": { + "com.cysharp.unitask": { + "version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", + "depth": 0, + "source": "git", + "dependencies": {}, + "hash": "f2773f585e762480c835ac718b82b87cb111e500" + }, "com.unity.2d.animation": { "version": "7.0.10", "depth": 1, @@ -154,6 +161,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.2.1", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.test-framework": { "version": "1.1.31", "depth": 0,