-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLLM_NPC_Controller.cs
More file actions
272 lines (245 loc) · 11.6 KB
/
LLM_NPC_Controller.cs
File metadata and controls
272 lines (245 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
using UnityEngine;
using UnityEngine.UI;
using System.Collections; // Äëÿ êîðóòèí
using UnityEngine.Networking; // Äëÿ UnityWebRequest (HTTP çàïðîñû)
using LitJson; // Äëÿ ïàðñèíãà JSON îòâåòîâ îò LLM (òðåáóåòñÿ LitJson.dll â Assets/Plugins)
using System.Text; // Äëÿ ðàáîòû ñ êîäèðîâêàìè ñòðîê
// Èíòåðôåéñ, êîòîðûé âàø NPC äîëæåí ðåàëèçîâàòü, ÷òîáû ïðèíèìàòü êîìàíäû îò LLM
/*public interface ILLMControllableNPC
{
void MoveToPosition(Vector3 position);
void MoveToObject(GameObject targetObject);
void SetBehavior(string behaviorType); // Íàïðèìåð, "Patrol", "Idle", "ChasePlayer"
void SayDialogue(string text);
// ... äðóãèå êîìàíäû, êîòîðûå âû õîòèòå, ÷òîáû NPC âûïîëíÿë
}*/
public class LLM_NPC_Controller : MonoBehaviour
{
[Header("UI ññûëêè")]
[Tooltip("Ïîëå ââîäà òåêñòà äëÿ êîìàíä NPC.")]
public InputField commandInputField;
[Tooltip("Êíîïêà äëÿ îòïðàâêè êîìàíäû NPC.")]
public Button sendCommandButton;
[Tooltip("Ïîëå äëÿ îòîáðàæåíèÿ îòâåòîâ îò LLM/ñòàòóñà.")]
public Text responseText;
[Header("Íàñòðîéêè LLM API")]
[Tooltip("URL âàøåãî LLM API (íàïðèìåð, Gemini API).")]
public string llmApiUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=";
[Tooltip("Âàø API-êëþ÷ äëÿ LLM (íå õðàíèòå â îòêðûòîì âèäå â ïðîäàêøåíå!).")]
public string apiKey = ""; // Çäåñü äîëæåí áûòü âàø API-êëþ÷
[Header("Ññûëêè íà NPC")]
[Tooltip("Ññûëêà íà NPC, êîòîðûì áóäåò óïðàâëÿòü LLM. Ó íåãî äîëæåí áûòü êîìïîíåíò, ðåàëèçóþùèé ILLMControllableNPC.")]
public GameObject targetNPCGameObject;
private ILLMControllableNPC targetNPC;
void Start()
{
/* if (commandInputField == null || sendCommandButton == null || responseText == null)
{
Debug.LogError("LLM_NPC_Controller: UI ýëåìåíòû íå íàçíà÷åíû!");
enabled = false;
return;
}
if (targetNPCGameObject == null)
{
Debug.LogError("LLM_NPC_Controller: Target NPC GameObject íå íàçíà÷åí!");
enabled = false;
return;
}
targetNPC = targetNPCGameObject.GetComponent<ILLMControllableNPC>();
if (targetNPC == null)
{
Debug.LogError($"LLM_NPC_Controller: Target NPC GameObject '{targetNPCGameObject.name}' íå ðåàëèçóåò èíòåðôåéñ ILLMControllableNPC!");
enabled = false;
return;
} */
sendCommandButton.onClick.AddListener(OnSendCommand);
responseText.text = "Ãîòîâ ê êîìàíäàì...";
}
/// <summary>
/// Âûçûâàåòñÿ ïðè íàæàòèè êíîïêè îòïðàâêè êîìàíäû.
/// </summary>
public void OnSendCommand()
{
string commandText = commandInputField.text;
if (string.IsNullOrWhiteSpace(commandText))
{
responseText.text = "Ââåäèòå êîìàíäó!";
return;
}
responseText.text = "Îòïðàâêà êîìàíäû LLM...";
StartCoroutine(SendToLLM(commandText)); // Çàïóñêàåì êîðóòèíó äëÿ îòïðàâêè çàïðîñà
commandInputField.text = ""; // Î÷èùàåì ïîëå ââîäà
}
/// <summary>
/// Îòïðàâëÿåò çàïðîñ LLM API è îáðàáàòûâàåò îòâåò.
/// </summary>
/// <param name="userPrompt">Òåêñòîâàÿ êîìàíäà ïîëüçîâàòåëÿ.</param>
IEnumerator SendToLLM(string userPrompt)
{
// === Øàã 1: Ôîðìèðîâàíèå çàïðîñà äëÿ LLM ===
// Çäåñü ìû ïðîñèì LLM îòâåòèòü â ñòðóêòóðèðîâàííîì JSON-ôîðìàòå
// Ýòî Î×ÅÍÜ ÂÀÆÍÎ, ÷òîáû LLM ãåíåðèðîâàëà ïàðñèðóåìûé îòâåò.
string llmPrompt = $"You are an AI assistant controlling a game NPC. The user will give you a command for the NPC. " +
$"Your task is to convert this command into a JSON object with a 'command' field and relevant 'parameters'. " +
$"Possible commands: 'moveToObject' (parameters: objectName:string), 'moveToPosition' (parameters: x:float, y:float, z:float), " +
$"'setBehavior' (parameters: behaviorType:string - e.g., 'Patrol', 'Idle', 'ChasePlayer'), " +
$"'say' (parameters: text:string). " +
$"If the command is unclear, use 'say' and ask for clarification. " +
$"Strictly respond ONLY with the JSON object. Do not include any other text.\n" +
$"User command: \"{userPrompt}\"";
// Åñëè âû èñïîëüçóåòå ìîäåëü, êîòîðàÿ ïîääåðæèâàåò responseSchema, èñïîëüçóéòå ýòî âìåñòî òåêñòîâîãî ïðîìïòà:
/*
const string payload = JsonMapper.ToJson(new {
contents = new[] {
new {
role = "user",
parts = new[] {
new { text = userPrompt }
}
}
},
generationConfig = new {
responseMimeType = "application/json",
responseSchema = new {
type = "OBJECT",
properties = new {
command = new { type = "STRING" },
parameters = new {
type = "OBJECT",
additionalProperties = true
}
},
required = new[] { "command" }
}
}
});
*/
// Äëÿ gemini-2.0-flash, êîòîðûé ìû èñïîëüçóåì, ëó÷øå âñåãî ñðàáîòàåò ïðîìïò â òåêñòå
string requestBody = JsonMapper.ToJson(new
{
contents = new[] {
new {
role = "user",
parts = new[] {
new { text = llmPrompt }
}
}
}
});
// Debug.Log($"Sending to LLM: {requestBody}");
UnityWebRequest request = new UnityWebRequest(llmApiUrl + apiKey, "POST");
byte[] bodyRaw = Encoding.UTF8.GetBytes(requestBody);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest(); // Îòïðàâëÿåì çàïðîñ è æäåì îòâåòà
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"Error sending to LLM: {request.error}");
responseText.text = $"Îøèáêà LLM: {request.error}";
}
else
{
string jsonResponse = request.downloadHandler.text;
Debug.Log($"Received from LLM: {jsonResponse}");
ProcessLLMResponse(jsonResponse); // Îáðàáàòûâàåì îòâåò
}
}
/// <summary>
/// Ðàçáèðàåò JSON-îòâåò îò LLM è âûçûâàåò ñîîòâåòñòâóþùèå êîìàíäû íà NPC.
/// </summary>
/// <param name="jsonResponse">JSON-ñòðîêà îòâåòà îò LLM.</param>
void ProcessLLMResponse(string jsonResponse)
{
try
{
// JsonMapper îæèäàåò, ÷òî êîðåíü JSON áóäåò îáúåêòîì èëè ìàññèâîì.
// Îòâåò îò Gemini API ÷àñòî îáåðíóò â ñòðóêòóðó: { candidates: [ { content: { parts: [ { text: "..." } ] } } ] }
JsonData rootData = JsonMapper.ToObject(jsonResponse);
if (rootData == null || !rootData.ContainsKey("candidates") || rootData["candidates"].Count == 0 ||
!rootData["candidates"][0].ContainsKey("content") || !rootData["candidates"][0]["content"].ContainsKey("parts") ||
rootData["candidates"][0]["content"]["parts"].Count == 0)
{
responseText.text = "Îøèáêà: Íåêîððåêòíûé îòâåò îò LLM.";
Debug.LogError("LLM response structure invalid: " + jsonResponse);
return;
}
// Èçâëåêàåì òåêñò JSON-êîìàíäû èç îòâåòà LLM
string commandJsonText = rootData["candidates"][0]["content"]["parts"][0]["text"].ToString();
Debug.Log("Parsed command JSON text: " + commandJsonText);
// Òåïåðü ïàðñèì ñàì JSON-îáúåêò êîìàíäû
JsonData commandData = JsonMapper.ToObject(commandJsonText);
string command = commandData["command"].ToString();
JsonData parameters = commandData.ContainsKey("parameters") ? commandData["parameters"] : null;
switch (command)
{
case "moveToObject":
if (parameters != null && parameters.ContainsKey("objectName"))
{
string objectName = parameters["objectName"].ToString();
GameObject targetObject = GameObject.Find(objectName); // Èùåì îáúåêò ïî èìåíè
if (targetObject != null)
{
targetNPC.MoveToObject(targetObject);
responseText.text = $"NPC äâèæåòñÿ ê {objectName}.";
}
else
{
responseText.text = $"Íå ìîãó íàéòè îáúåêò: {objectName}.";
}
}
else
{
responseText.text = "Îøèáêà: Äëÿ moveToObject òðåáóåòñÿ 'objectName'.";
}
break;
case "moveToPosition":
if (parameters != null && parameters.ContainsKey("x") && parameters.ContainsKey("y") && parameters.ContainsKey("z"))
{
float x = (float)parameters["x"];
float y = (float)parameters["y"];
float z = (float)parameters["z"];
targetNPC.MoveToPosition(new Vector3(x, y, z));
responseText.text = $"NPC äâèæåòñÿ ê ïîçèöèè ({x}, {y}, {z}).";
}
else
{
responseText.text = "Îøèáêà: Äëÿ moveToPosition òðåáóþòñÿ 'x', 'y', 'z'.";
}
break;
case "setBehavior":
if (parameters != null && parameters.ContainsKey("behaviorType"))
{
string behaviorType = parameters["behaviorType"].ToString();
targetNPC.SetBehavior(behaviorType);
responseText.text = $"NPC ïåðåõîäèò â ðåæèì: {behaviorType}.";
}
else
{
responseText.text = "Îøèáêà: Äëÿ setBehavior òðåáóåòñÿ 'behaviorType'.";
}
break;
case "say":
if (parameters != null && parameters.ContainsKey("text"))
{
string text = parameters["text"].ToString();
targetNPC.SayDialogue(text);
responseText.text = $"NPC ãîâîðèò: \"{text}\"";
}
else
{
responseText.text = "Îøèáêà: Äëÿ say òðåáóåòñÿ 'text'.";
}
break;
default:
responseText.text = $"Íåèçâåñòíàÿ êîìàíäà LLM: {command}.";
break;
}
}
catch (System.Exception e)
{
responseText.text = "Îøèáêà ïàðñèíãà îòâåòà LLM.";
Debug.LogError($"Error processing LLM response: {e.Message}\nResponse: {jsonResponse}");
}
}
}