Creating NPCs Tutorial
Learn how to create engaging Non-Player Characters with dialogue, quests, shops, and AI behavior.
NPC System Overview
The NPC system provides a comprehensive framework for creating interactive characters that can engage in dialogue, give quests, run shops, and exhibit various behaviors.
💬 Dialogue NPCs
Characters that engage in conversations with branching dialogue trees
❗ Quest Givers
NPCs that provide quests and track quest completion
🏪 Shopkeepers
Merchants that sell items and equipment to players
🤖 AI Characters
NPCs with autonomous behavior, schedules, and reactions
NPC System Architecture
Core NPC Component
Basic NPC functionality and interaction system
Dialogue System
Conversation management and branching dialogue
Quest Integration
Quest giving and completion tracking
Shop System
Item trading and merchant functionality
AI Behavior
Autonomous movement and schedule system
Basic NPC Setup
Step 1: Core NPC Component
Start with the fundamental NPC script that handles basic interactions:
1"keyword">using UnityEngine;
2"keyword">using System.Collections;
3
4"keyword">public "keyword">class NPC : MonoBehaviour, IInteractable
5{
6 [Header("NPC Identity")]
7 "keyword">public "keyword">string npcName = "Villager";
8 [TextArea(2, 4)]
9 "keyword">public "keyword">string description = "A friendly village resident";
10 "keyword">public NPCType npcType = NPCType.Villager;
11
12 [Header("Dialogue")]
13 "keyword">public DialogueTree dialogueTree;
14 "keyword">public "keyword">string[] casualGreetings = {"Hello there!", "Good day!", "How can I help?"};
15
16 [Header("Quest System")]
17 "keyword">public Quest questToGive;
18 "keyword">public Quest[] availableQuests;
19 "keyword">public "keyword">bool hasGivenQuest = false;
20 "keyword">public QuestStatus questStatus = QuestStatus.Available;
21
22 [Header("Shop System")]
23 "keyword">public "keyword">bool isShopkeeper = false;
24 "keyword">public Shop npcShop;
25 "keyword">public Item[] shopItems;
26
27 [Header("Interaction")]
28 "keyword">public GameObject interactionPrompt;
29 "keyword">public "keyword">float interactionRange = 2f;
30 "keyword">public KeyCode interactionKey = KeyCode.E;
31 "keyword">public "keyword">string interactionText = "Press E to interact";
32
33 [Header("Visual & Audio")]
34 "keyword">public SpriteRenderer spriteRenderer;
35 "keyword">public Animator animator;
36 "keyword">public AudioSource audioSource;
37 "keyword">public AudioClip[] voiceClips;
38
39 [Header("Movement")]
40 "keyword">public "keyword">bool allowMovement = true;
41 "keyword">public "keyword">float moveSpeed = 1f;
42 "keyword">public Transform[] patrolPoints;
43 "keyword">public "keyword">float waitTime = 2f;
44
45 "comment">// Private variables
46 "keyword">private "keyword">bool playerInRange = false;
47 "keyword">private PlayerController nearbyPlayer;
48 "keyword">private Coroutine movementCoroutine;
49 "keyword">private "keyword">int currentPatrolIndex = 0;
50
51 "comment">// Events
52 "keyword">public System.Action<NPC> OnInteracted;
53 "keyword">public System.Action<NPC, Quest> OnQuestGiven;
54 "keyword">public System.Action<NPC> OnShopOpened;
55
56 "keyword">void Start()
57 {
58 InitializeNPC();
59 }
60
61 "keyword">void InitializeNPC()
62 {
63 "comment">// Setup components
64 "keyword">if (spriteRenderer == null)
65 spriteRenderer = GetComponent<SpriteRenderer>();
66
67 "keyword">if (animator == null)
68 animator = GetComponent<Animator>();
69
70 "keyword">if (audioSource == null)
71 audioSource = GetComponent<AudioSource>();
72
73 "comment">// Setup interaction prompt
74 "keyword">if (interactionPrompt != null)
75 {
76 interactionPrompt.SetActive(false);
77 }
78
79 "comment">// Initialize shop "keyword">if shopkeeper
80 "keyword">if (isShopkeeper && npcShop == null && shopItems.Length > 0)
81 {
82 CreateShopFromItems();
83 }
84
85 "comment">// Start movement behavior
86 "keyword">if (allowMovement && patrolPoints.Length > 0)
87 {
88 movementCoroutine = StartCoroutine(PatrolBehavior());
89 }
90
91 "comment">// Subscribe to quest events
92 "keyword">if (QuestManager.Instance != null)
93 {
94 QuestManager.Instance.OnQuestCompleted += OnQuestCompleted;
95 QuestManager.Instance.OnQuestStarted += OnQuestStarted;
96 }
97
98 Debug.Log($"NPC {npcName} initialized successfully");
99 }
100
101 "keyword">void Update()
102 {
103 CheckPlayerProximity();
104 HandleInput();
105 UpdateAnimations();
106 }
107
108 "keyword">void CheckPlayerProximity()
109 {
110 var player = FindObjectOfType<PlayerController>();
111 "keyword">if (player == null) "keyword">return;
112
113 "keyword">float distance = Vector2.Distance(transform.position, player.transform.position);
114 "keyword">bool wasInRange = playerInRange;
115 playerInRange = distance <= interactionRange;
116
117 "keyword">if (playerInRange != wasInRange)
118 {
119 "keyword">if (playerInRange)
120 {
121 OnPlayerEnterRange(player);
122 }
123 "keyword">else
124 {
125 OnPlayerExitRange(player);
126 }
127 }
128
129 nearbyPlayer = playerInRange ? player : null;
130 }
131
132 "keyword">void OnPlayerEnterRange(PlayerController player)
133 {
134 "keyword">if (interactionPrompt != null)
135 {
136 interactionPrompt.SetActive(true);
137 }
138
139 "comment">// Play greeting sound occasionally
140 "keyword">if (Random.value < 0.3f && voiceClips.Length > 0)
141 {
142 PlayVoiceClip(voiceClips[Random.Range(0, voiceClips.Length)]);
143 }
144
145 "comment">// Update interaction text
146 UpdateInteractionText();
147
148 Debug.Log($"Player entered {npcName}'s interaction range");
149 }
150
151 "keyword">void OnPlayerExitRange(PlayerController player)
152 {
153 "keyword">if (interactionPrompt != null)
154 {
155 interactionPrompt.SetActive(false);
156 }
157
158 Debug.Log($"Player exited {npcName}'s interaction range");
159 }
160
161 "keyword">void HandleInput()
162 {
163 "keyword">if (playerInRange && Input.GetKeyDown(interactionKey))
164 {
165 Interact();
166 }
167 }
168
169 "keyword">void UpdateAnimations()
170 {
171 "keyword">if (animator == null) "keyword">return;
172
173 "comment">// Update animator parameters based on NPC state
174 animator.SetBool("PlayerNearby", playerInRange);
175 animator.SetBool("IsMoving", movementCoroutine != null);
176 animator.SetBool("HasQuest", HasAvailableQuest());
177 animator.SetBool("IsShopkeeper", isShopkeeper);
178 }
179
180 "comment">// IInteractable Implementation
181 "keyword">public "keyword">void Interact()
182 {
183 "keyword">if (!CanInteract()) "keyword">return;
184
185 "comment">// Trigger interaction event
186 OnInteracted?.Invoke("keyword">this);
187
188 "comment">// Face the player
189 FacePlayer();
190
191 "comment">// Play interaction sound
192 PlayInteractionSound();
193
194 "comment">// Handle different interaction types
195 HandleInteraction();
196 }
197
198 "keyword">void HandleInteraction()
199 {
200 "comment">// Priority order: Quest -> Dialogue -> Shop
201 "keyword">if (HasAvailableQuest())
202 {
203 HandleQuestInteraction();
204 }
205 "keyword">else "keyword">if (dialogueTree != null)
206 {
207 StartDialogue();
208 }
209 "keyword">else "keyword">if (isShopkeeper)
210 {
211 OpenShop();
212 }
213 "keyword">else
214 {
215 "comment">// Default casual interaction
216 ShowCasualGreeting();
217 }
218 }
219
220 "keyword">public "keyword">string GetInteractionText()
221 {
222 "keyword">if (HasAvailableQuest())
223 {
224 "keyword">return $"Talk to {npcName} (Quest Available!)";
225 }
226 "keyword">else "keyword">if (questStatus == QuestStatus.InProgress)
227 {
228 "keyword">return $"Talk to {npcName} (Quest In Progress)";
229 }
230 "keyword">else "keyword">if (isShopkeeper)
231 {
232 "keyword">return $"Shop with {npcName}";
233 }
234 "keyword">else "keyword">if (dialogueTree != null)
235 {
236 "keyword">return $"Talk to {npcName}";
237 }
238 "keyword">else
239 {
240 "keyword">return interactionText;
241 }
242 }
243
244 "keyword">public "keyword">bool CanInteract()
245 {
246 "keyword">return playerInRange &&
247 GameManager.Instance.currentState == GameState.Playing &&
248 !DialogueManager.Instance.IsDialogueActive();
249 }
250
251 "keyword">void UpdateInteractionText()
252 {
253 "keyword">string newText = GetInteractionText();
254
255 "comment">// Update UI "keyword">if you have an interaction UI system
256 "keyword">if (UIManager.Instance != null)
257 {
258 UIManager.Instance.UpdateInteractionPrompt(newText);
259 }
260 }
261
262 "keyword">void FacePlayer()
263 {
264 "keyword">if (nearbyPlayer != null && spriteRenderer != null)
265 {
266 Vector3 direction = nearbyPlayer.transform.position - transform.position;
267
268 "comment">// Flip sprite based on player position
269 "keyword">if (direction.x < 0)
270 {
271 spriteRenderer.flipX = true;
272 }
273 "keyword">else "keyword">if (direction.x > 0)
274 {
275 spriteRenderer.flipX = false;
276 }
277 }
278 }
279
280 "keyword">void PlayInteractionSound()
281 {
282 "keyword">if (audioSource != null && voiceClips.Length > 0)
283 {
284 PlayVoiceClip(voiceClips[Random.Range(0, voiceClips.Length)]);
285 }
286 }
287
288 "keyword">void PlayVoiceClip(AudioClip clip)
289 {
290 "keyword">if (audioSource != null && clip != null)
291 {
292 audioSource.clip = clip;
293 audioSource.pitch = Random.Range(0.9f, 1.1f); "comment">// Slight pitch variation
294 audioSource.Play();
295 }
296 }
297
298 "comment">// Utility Methods
299 "keyword">public "keyword">bool HasAvailableQuest()
300 {
301 "keyword">return (questToGive != null && !hasGivenQuest) ||
302 (availableQuests.Length > 0 && questStatus == QuestStatus.Available);
303 }
304
305 "keyword">void CreateShopFromItems()
306 {
307 "keyword">if (shopItems.Length == 0) "keyword">return;
308
309 npcShop = ScriptableObject.CreateInstance<Shop>();
310 npcShop.shopName = $"{npcName}'s Shop";
311 npcShop.shopkeeper = npcName;
312 npcShop.items = "keyword">new System.Collections.Generic.List<ShopItem>();
313
314 "keyword">foreach (var item in shopItems)
315 {
316 var shopItem = "keyword">new ShopItem
317 {
318 item = item,
319 price = item.value,
320 stock = -1, "comment">// Infinite stock
321 available = true
322 };
323 npcShop.items.Add(shopItem);
324 }
325 }
326
327 "keyword">void OnDrawGizmosSelected()
328 {
329 "comment">// Draw interaction range
330 Gizmos.color = Color.blue;
331 Gizmos.DrawWireSphere(transform.position, interactionRange);
332
333 "comment">// Draw patrol points
334 "keyword">if (patrolPoints != null && patrolPoints.Length > 0)
335 {
336 Gizmos.color = Color.green;
337 "keyword">for ("keyword">int i = 0; i < patrolPoints.Length; i++)
338 {
339 "keyword">if (patrolPoints[i] != null)
340 {
341 Gizmos.DrawWireSphere(patrolPoints[i].position, 0.3f);
342
343 "comment">// Draw lines between patrol points
344 "keyword">int nextIndex = (i + 1) % patrolPoints.Length;
345 "keyword">if (patrolPoints[nextIndex] != null)
346 {
347 Gizmos.DrawLine(patrolPoints[i].position, patrolPoints[nextIndex].position);
348 }
349 }
350 }
351 }
352 }
353
354 "keyword">void OnDestroy()
355 {
356 "comment">// Unsubscribe from events
357 "keyword">if (QuestManager.Instance != null)
358 {
359 QuestManager.Instance.OnQuestCompleted -= OnQuestCompleted;
360 QuestManager.Instance.OnQuestStarted -= OnQuestStarted;
361 }
362 }
363}
364
365"comment">// Enumerations
366"keyword">public "keyword">enum NPCType
367{
368 Villager,
369 Merchant,
370 Guard,
371 Noble,
372 Warrior,
373 Mage,
374 Priest,
375 Child,
376 Elder,
377 Custom
378}
379
380"keyword">public "keyword">enum QuestStatus
381{
382 Available,
383 Given,
384 InProgress,
385 Completed,
386 Failed
387}
Step 2: Setting Up NPC GameObject
Create the complete NPC GameObject with all necessary components:
- Create GameObject: Right-click in Hierarchy → Create Empty → Name it "NPC_[Name]"
- Add Visual Components:
- SpriteRenderer - For the NPC's appearance
- Animator - For animations (optional)
- Add Physics Components:
- Collider2D - For interaction detection
- Rigidbody2D - If NPC needs to move (set to Kinematic)
- Add Audio:
- AudioSource - For voice clips and sound effects
- Add Scripts:
- NPC script (the main component)
NPC Setup Checklist:
- □ GameObject created with descriptive name
- □ SpriteRenderer added with NPC sprite
- □ Collider2D configured (trigger for interaction)
- □ NPC script attached and configured
- □ Interaction range set appropriately
- □ NPC name and description filled in
- □ Audio components added if needed
- □ Layer and tags set correctly
Dialogue NPCs
Creating Conversational NPCs
Add dialogue capabilities to make NPCs more engaging:
1"comment">// Add these methods to the NPC "keyword">class "keyword">for dialogue handling
2
3"keyword">void StartDialogue()
4{
5 "keyword">if (dialogueTree == null)
6 {
7 ShowCasualGreeting();
8 "keyword">return;
9 }
10
11 "comment">// Set NPC context "keyword">for dialogue system
12 var dialogueContext = "keyword">new DialogueContext
13 {
14 speaker = "keyword">this,
15 playerLevel = GameManager.Instance.GetPlayerLevel(),
16 completedQuests = QuestManager.Instance.GetCompletedQuests(),
17 playerInventory = InventoryManager.Instance.GetInventory()
18 };
19
20 DialogueManager.Instance.StartDialogue(dialogueTree, dialogueContext);
21}
22
23"keyword">void ShowCasualGreeting()
24{
25 "keyword">if (casualGreetings.Length == 0) "keyword">return;
26
27 "keyword">string greeting = casualGreetings[Random.Range(0, casualGreetings.Length)];
28
29 "comment">// Show greeting in UI
30 "keyword">if (UIManager.Instance != null)
31 {
32 UIManager.Instance.ShowFloatingText(transform.position, greeting, Color.white);
33 }
34
35 "comment">// Play voice clip
36 "keyword">if (voiceClips.Length > 0)
37 {
38 PlayVoiceClip(voiceClips[Random.Range(0, voiceClips.Length)]);
39 }
40
41 Debug.Log($"{npcName} says: {greeting}");
42}
43
44"comment">// Dialogue event handlers
45"keyword">public "keyword">void OnDialogueStarted()
46{
47 "comment">// Pause NPC movement during dialogue
48 "keyword">if (movementCoroutine != null)
49 {
50 StopCoroutine(movementCoroutine);
51 movementCoroutine = null;
52 }
53
54 "comment">// Face player
55 FacePlayer();
56
57 "comment">// Update animation state
58 "keyword">if (animator != null)
59 {
60 animator.SetBool("InDialogue", true);
61 }
62}
63
64"keyword">public "keyword">void OnDialogueEnded()
65{
66 "comment">// Resume movement
67 "keyword">if (allowMovement && patrolPoints.Length > 0)
68 {
69 movementCoroutine = StartCoroutine(PatrolBehavior());
70 }
71
72 "comment">// Update animation state
73 "keyword">if (animator != null)
74 {
75 animator.SetBool("InDialogue", false);
76 }
77}
78
79"comment">// Context-sensitive dialogue
80"keyword">public "keyword">string GetContextualDialogue()
81{
82 var player = GameManager.Instance.GetPlayer();
83 "keyword">if (player == null) "keyword">return GetRandomGreeting();
84
85 "comment">// Check player state and provide appropriate dialogue
86 "keyword">if (player.GetComponent<PlayerStats>().currentHealth < player.GetComponent<PlayerStats>().maxHealth * 0.3f)
87 {
88 "keyword">return "You look hurt! You should find a healer.";
89 }
90
91 "keyword">if (QuestManager.Instance.HasActiveQuest())
92 {
93 "keyword">return "Good luck with your current quest, adventurer!";
94 }
95
96 "keyword">if (IsNightTime())
97 {
98 "keyword">return "It's getting late. Be careful out there in the dark.";
99 }
100
101 "keyword">return GetRandomGreeting();
102}
103
104"keyword">string GetRandomGreeting()
105{
106 "keyword">return casualGreetings.Length > 0 ?
107 casualGreetings[Random.Range(0, casualGreetings.Length)] :
108 "Hello there!";
109}
110
111"keyword">bool IsNightTime()
112{
113 "comment">// Integrate with day/night cycle "keyword">if available
114 var dayNightCycle = FindObjectOfType<DayNightCycle>();
115 "keyword">return dayNightCycle != null && dayNightCycle.IsNight();
116}
Advanced Dialogue Features
Implement more sophisticated dialogue mechanics:
1[System.Serializable]
2"keyword">public "keyword">class NPCDialogueState
3{
4 "keyword">public "keyword">string stateName;
5 "keyword">public DialogueTree dialogueTree;
6 "keyword">public DialogueCondition[] conditions;
7 "keyword">public "keyword">bool isDefault = false;
8}
9
10[System.Serializable]
11"keyword">public "keyword">class DialogueCondition
12{
13 "keyword">public ConditionType type;
14 "keyword">public "keyword">string targetId;
15 "keyword">public ComparisonOperator comparison;
16 "keyword">public "keyword">float value;
17 "keyword">public "keyword">bool expectedBoolValue;
18}
19
20"keyword">public "keyword">enum ConditionType
21{
22 QuestCompleted,
23 QuestActive,
24 PlayerLevel,
25 HasItem,
26 TimeOfDay,
27 Relationship,
28 Custom
29}
30
31"keyword">public "keyword">enum ComparisonOperator
32{
33 Equal,
34 NotEqual,
35 Greater,
36 Less,
37 GreaterOrEqual,
38 LessOrEqual
39}
40
41"comment">// Add to NPC "keyword">class
42[Header("Advanced Dialogue")]
43"keyword">public NPCDialogueState[] dialogueStates;
44"keyword">public "keyword">float relationshipLevel = 0f;
45"keyword">public "keyword">string[] memorizedPlayerActions;
46
47DialogueTree GetAppropriateDialogue()
48{
49 "comment">// Check dialogue states in order
50 "keyword">foreach (var state in dialogueStates)
51 {
52 "keyword">if (EvaluateDialogueConditions(state.conditions))
53 {
54 "keyword">return state.dialogueTree;
55 }
56 }
57
58 "comment">// Return default dialogue
59 var defaultState = System.Array.Find(dialogueStates, s => s.isDefault);
60 "keyword">return defaultState?.dialogueTree ?? dialogueTree;
61}
62
63"keyword">bool EvaluateDialogueConditions(DialogueCondition[] conditions)
64{
65 "keyword">if (conditions == null || conditions.Length == 0) "keyword">return true;
66
67 "keyword">foreach (var condition in conditions)
68 {
69 "keyword">if (!EvaluateCondition(condition))
70 {
71 "keyword">return false;
72 }
73 }
74
75 "keyword">return true;
76}
77
78"keyword">bool EvaluateCondition(DialogueCondition condition)
79{
80 switch (condition.type)
81 {
82 case ConditionType.QuestCompleted:
83 "keyword">return QuestManager.Instance.IsQuestCompleted(condition.targetId);
84
85 case ConditionType.QuestActive:
86 "keyword">return QuestManager.Instance.IsQuestActive(condition.targetId);
87
88 case ConditionType.PlayerLevel:
89 "keyword">int playerLevel = GameManager.Instance.GetPlayerLevel();
90 "keyword">return CompareValues(playerLevel, condition.value, condition.comparison);
91
92 case ConditionType.HasItem:
93 "keyword">return InventoryManager.Instance.HasItem(condition.targetId);
94
95 case ConditionType.TimeOfDay:
96 "keyword">float currentTime = GetCurrentTimeOfDay();
97 "keyword">return CompareValues(currentTime, condition.value, condition.comparison);
98
99 case ConditionType.Relationship:
100 "keyword">return CompareValues(relationshipLevel, condition.value, condition.comparison);
101
102 default:
103 "keyword">return true;
104 }
105}
106
107"keyword">bool CompareValues("keyword">float a, "keyword">float b, ComparisonOperator op)
108{
109 switch (op)
110 {
111 case ComparisonOperator.Equal: "keyword">return Mathf.Approximately(a, b);
112 case ComparisonOperator.NotEqual: "keyword">return !Mathf.Approximately(a, b);
113 case ComparisonOperator.Greater: "keyword">return a > b;
114 case ComparisonOperator.Less: "keyword">return a < b;
115 case ComparisonOperator.GreaterOrEqual: "keyword">return a >= b;
116 case ComparisonOperator.LessOrEqual: "keyword">return a <= b;
117 default: "keyword">return false;
118 }
119}
120
121"keyword">float GetCurrentTimeOfDay()
122{
123 var dayNightCycle = FindObjectOfType<DayNightCycle>();
124 "keyword">return dayNightCycle?.GetCurrentTime() ?? 12f; "comment">// Default to noon
125}
126
127"comment">// Relationship system
128"keyword">public "keyword">void ModifyRelationship("keyword">float amount)
129{
130 relationshipLevel = Mathf.Clamp(relationshipLevel + amount, -100f, 100f);
131
132 "comment">// Trigger relationship events
133 OnRelationshipChanged(relationshipLevel);
134}
135
136"keyword">void OnRelationshipChanged("keyword">float newLevel)
137{
138 "keyword">if (newLevel >= 75f)
139 {
140 "comment">// Unlock friendly dialogue
141 UnlockDialogueState("Friendly");
142 }
143 "keyword">else "keyword">if (newLevel <= -50f)
144 {
145 "comment">// Switch to hostile dialogue
146 UnlockDialogueState("Hostile");
147 }
148}
149
150"keyword">void UnlockDialogueState("keyword">string stateName)
151{
152 var state = System.Array.Find(dialogueStates, s => s.stateName == stateName);
153 "keyword">if (state != null)
154 {
155 Debug.Log($"Unlocked dialogue state: {stateName} ">for {npcName}");
156 }
157}
Quest Givers
Quest Management System
Implement NPCs that can give and manage quests:
1"comment">// Add these quest-related methods to the NPC "keyword">class
2
3"keyword">void HandleQuestInteraction()
4{
5 "keyword">if (questToGive != null && !hasGivenQuest)
6 {
7 "comment">// Check "keyword">if player meets quest requirements
8 "keyword">if (CanGiveQuest(questToGive))
9 {
10 GiveQuestToPlayer(questToGive);
11 }
12 "keyword">else
13 {
14 ShowQuestRequirementMessage(questToGive);
15 }
16 }
17 "keyword">else "keyword">if (HasActiveQuestForThisNPC())
18 {
19 "comment">// Handle quest turn-in or progress check
20 HandleActiveQuest();
21 }
22 "keyword">else
23 {
24 "comment">// No quest available, proceed with normal dialogue
25 StartDialogue();
26 }
27}
28
29"keyword">bool CanGiveQuest(Quest quest)
30{
31 "keyword">if (quest == null) "keyword">return false;
32
33 var player = GameManager.Instance.GetPlayer();
34 "keyword">if (player == null) "keyword">return false;
35
36 var playerStats = player.GetComponent<PlayerStats>();
37
38 "comment">// Check level requirement
39 "keyword">if (playerStats.level < quest.levelRequirement)
40 {
41 "keyword">return false;
42 }
43
44 "comment">// Check prerequisite quests
45 "keyword">if (quest.prerequisiteQuests != null)
46 {
47 "keyword">foreach (var prereq in quest.prerequisiteQuests)
48 {
49 "keyword">if (!QuestManager.Instance.IsQuestCompleted(prereq.questId))
50 {
51 "keyword">return false;
52 }
53 }
54 }
55
56 "comment">// Check "keyword">if player already has "keyword">this quest or completed it
57 "keyword">if (QuestManager.Instance.IsQuestActive(quest.questId) ||
58 QuestManager.Instance.IsQuestCompleted(quest.questId))
59 {
60 "keyword">return false;
61 }
62
63 "keyword">return true;
64}
65
66"keyword">void GiveQuestToPlayer(Quest quest)
67{
68 "keyword">if (QuestManager.Instance.StartQuest(quest))
69 {
70 hasGivenQuest = true;
71 questStatus = QuestStatus.Given;
72
73 "comment">// Play quest give sound/animation
74 PlayQuestGiveEffects();
75
76 "comment">// Show quest notification
77 ShowQuestNotification(quest);
78
79 "comment">// Trigger event
80 OnQuestGiven?.Invoke("keyword">this, quest);
81
82 "comment">// Start quest dialogue "keyword">if available
83 "keyword">if (quest.introDialogue != null)
84 {
85 DialogueManager.Instance.StartDialogue(quest.introDialogue);
86 }
87
88 Debug.Log($"{npcName} gave quest: {quest.questName}");
89 }
90}
91
92"keyword">void ShowQuestRequirementMessage(Quest quest)
93{
94 "keyword">string message = GetQuestRequirementMessage(quest);
95
96 "keyword">if (UIManager.Instance != null)
97 {
98 UIManager.Instance.ShowNotification(message, NotificationType.Warning);
99 }
100
101 Debug.Log($"Quest requirement not met: {message}");
102}
103
104"keyword">string GetQuestRequirementMessage(Quest quest)
105{
106 var player = GameManager.Instance.GetPlayer();
107 var playerStats = player.GetComponent<PlayerStats>();
108
109 "keyword">if (playerStats.level < quest.levelRequirement)
110 {
111 "keyword">return $"You need to be level {quest.levelRequirement} to take ">this quest.";
112 }
113
114 "keyword">if (quest.prerequisiteQuests != null)
115 {
116 "keyword">foreach (var prereq in quest.prerequisiteQuests)
117 {
118 "keyword">if (!QuestManager.Instance.IsQuestCompleted(prereq.questId))
119 {
120 "keyword">return $"You must complete '{prereq.questName}' first.";
121 }
122 }
123 }
124
125 "keyword">return "You don't meet the requirements ">for ">this quest.";
126}
127
128"keyword">bool HasActiveQuestForThisNPC()
129{
130 "comment">// Check "keyword">if player has an active quest that involves "keyword">this NPC
131 var activeQuests = QuestManager.Instance.GetActiveQuests();
132
133 "keyword">foreach (var quest in activeQuests)
134 {
135 "keyword">if (quest.questGiver == npcName || IsQuestObjectiveTarget(quest))
136 {
137 "keyword">return true;
138 }
139 }
140
141 "keyword">return false;
142}
143
144"keyword">bool IsQuestObjectiveTarget(Quest quest)
145{
146 "keyword">if (quest.objectives == null) "keyword">return false;
147
148 "keyword">foreach (var objective in quest.objectives)
149 {
150 "keyword">if (objective.targetNPC == npcName || objective.targetId == npcName)
151 {
152 "keyword">return true;
153 }
154 }
155
156 "keyword">return false;
157}
158
159"keyword">void HandleActiveQuest()
160{
161 var activeQuests = QuestManager.Instance.GetActiveQuests();
162
163 "keyword">foreach (var quest in activeQuests)
164 {
165 "keyword">if (quest.questGiver == npcName)
166 {
167 "comment">// This is a quest "keyword">return
168 HandleQuestReturn(quest);
169 "keyword">return;
170 }
171 "keyword">else "keyword">if (IsQuestObjectiveTarget(quest))
172 {
173 "comment">// This NPC is involved in an objective
174 HandleQuestObjective(quest);
175 "keyword">return;
176 }
177 }
178}
179
180"keyword">void HandleQuestReturn(Quest quest)
181{
182 "keyword">if (QuestManager.Instance.IsQuestCompleted(quest))
183 {
184 "comment">// Quest is completed, handle turn-in
185 CompleteQuestTurnIn(quest);
186 }
187 "keyword">else "keyword">if (QuestManager.Instance.CanCompleteQuest(quest))
188 {
189 "comment">// All objectives done, but not officially completed
190 ShowQuestCompletionDialogue(quest);
191 }
192 "keyword">else
193 {
194 "comment">// Quest still in progress
195 ShowQuestProgressDialogue(quest);
196 }
197}
198
199"keyword">void CompleteQuestTurnIn(Quest quest)
200{
201 "comment">// Give rewards
202 GiveQuestRewards(quest);
203
204 "comment">// Mark quest as turned in
205 QuestManager.Instance.TurnInQuest(quest.questId);
206
207 "comment">// Update NPC state
208 questStatus = QuestStatus.Completed;
209
210 "comment">// Play completion effects
211 PlayQuestCompleteEffects();
212
213 "comment">// Show completion dialogue
214 "keyword">if (quest.completionDialogue != null)
215 {
216 DialogueManager.Instance.StartDialogue(quest.completionDialogue);
217 }
218
219 "comment">// Check "keyword">for follow-up quests
220 CheckForFollowUpQuests(quest);
221
222 Debug.Log($"Quest '{quest.questName}' turned in to {npcName}");
223}
224
225"keyword">void GiveQuestRewards(Quest quest)
226{
227 "keyword">if (quest.rewards == null) "keyword">return;
228
229 var player = GameManager.Instance.GetPlayer();
230 var playerStats = player.GetComponent<PlayerStats>();
231 var inventory = InventoryManager.Instance;
232
233 "keyword">foreach (var reward in quest.rewards)
234 {
235 switch (reward.rewardType)
236 {
237 case RewardType.Experience:
238 playerStats.GainExperience(("keyword">int)reward.amount);
239 ShowRewardNotification($"Gained {reward.amount} XP!");
240 break;
241
242 case RewardType.Gold:
243 inventory.AddGold(("keyword">int)reward.amount);
244 ShowRewardNotification($"Received {reward.amount} gold!");
245 break;
246
247 case RewardType.Item:
248 "keyword">if (reward.item != null)
249 {
250 inventory.AddItem(reward.item, ("keyword">int)reward.amount);
251 ShowRewardNotification($"Received {reward.item.itemName}!");
252 }
253 break;
254
255 case RewardType.Reputation:
256 "comment">// Handle reputation system "keyword">if available
257 ModifyRelationship(reward.amount);
258 ShowRewardNotification($"Reputation changed by {reward.amount}!");
259 break;
260 }
261 }
262}
263
264"keyword">void ShowQuestProgressDialogue(Quest quest)
265{
266 "comment">// Show current progress and hints
267 "keyword">string progressMessage = GetQuestProgressMessage(quest);
268
269 "keyword">if (quest.progressDialogue != null)
270 {
271 DialogueManager.Instance.StartDialogue(quest.progressDialogue);
272 }
273 "keyword">else
274 {
275 ShowCasualMessage(progressMessage);
276 }
277}
278
279"keyword">string GetQuestProgressMessage(Quest quest)
280{
281 var completedObjectives = QuestManager.Instance.GetCompletedObjectives(quest);
282 var totalObjectives = quest.objectives.Length;
283
284 "keyword">return $"You've completed {completedObjectives.Length} of {totalObjectives} objectives. Keep going!";
285}
286
287"keyword">void CheckForFollowUpQuests(Quest completedQuest)
288{
289 "comment">// Check "keyword">if "keyword">this NPC has any follow-up quests
290 "keyword">if (availableQuests != null)
291 {
292 "keyword">foreach (var followUpQuest in availableQuests)
293 {
294 "keyword">if (IsFollowUpQuest(followUpQuest, completedQuest) && CanGiveQuest(followUpQuest))
295 {
296 questToGive = followUpQuest;
297 hasGivenQuest = false;
298 questStatus = QuestStatus.Available;
299
300 "comment">// Notify player of "keyword">new quest
301 ShowFollowUpQuestNotification(followUpQuest);
302 break;
303 }
304 }
305 }
306}
307
308"keyword">bool IsFollowUpQuest(Quest followUp, Quest completed)
309{
310 "keyword">if (followUp.prerequisiteQuests == null) "keyword">return false;
311
312 "keyword">return System.Array.Exists(followUp.prerequisiteQuests,
313 prereq => prereq.questId == completed.questId);
314}
315
316"keyword">void PlayQuestGiveEffects()
317{
318 "comment">// Visual effects
319 "keyword">if (UIManager.Instance != null)
320 {
321 UIManager.Instance.PlayQuestGiveEffect(transform.position);
322 }
323
324 "comment">// Audio
325 AudioManager.Instance?.PlaySFX("QuestGive", transform);
326
327 "comment">// Animation
328 "keyword">if (animator != null)
329 {
330 animator.SetTrigger("GiveQuest");
331 }
332}
333
334"keyword">void PlayQuestCompleteEffects()
335{
336 "comment">// Visual effects
337 "keyword">if (UIManager.Instance != null)
338 {
339 UIManager.Instance.PlayQuestCompleteEffect(transform.position);
340 }
341
342 "comment">// Audio
343 AudioManager.Instance?.PlaySFX("QuestComplete", transform);
344
345 "comment">// Animation
346 "keyword">if (animator != null)
347 {
348 animator.SetTrigger("CompleteQuest");
349 }
350}
351
352"comment">// Quest event handlers
353"keyword">void OnQuestStarted(Quest quest)
354{
355 "keyword">if (quest.questGiver == npcName)
356 {
357 questStatus = QuestStatus.InProgress;
358 UpdateInteractionText();
359 }
360}
361
362"keyword">void OnQuestCompleted(Quest quest)
363{
364 "keyword">if (quest.questGiver == npcName)
365 {
366 questStatus = QuestStatus.Completed;
367 UpdateInteractionText();
368
369 "comment">// Show exclamation mark or similar indicator
370 ShowQuestCompleteIndicator();
371 }
372}
373
374"keyword">void ShowQuestCompleteIndicator()
375{
376 "comment">// Visual indicator that quest can be turned in
377 "keyword">if (interactionPrompt != null)
378 {
379 var indicator = interactionPrompt.transform.Find("QuestCompleteIndicator");
380 "keyword">if (indicator != null)
381 {
382 indicator.gameObject.SetActive(true);
383 }
384 }
385}
386
387"keyword">void ShowRewardNotification("keyword">string message)
388{
389 "keyword">if (UIManager.Instance != null)
390 {
391 UIManager.Instance.ShowFloatingText(transform.position + Vector3.up, message, Color.gold);
392 }
393}
394
395"keyword">void ShowFollowUpQuestNotification(Quest quest)
396{
397 "keyword">if (UIManager.Instance != null)
398 {
399 UIManager.Instance.ShowNotification($"New quest available from {npcName}: {quest.questName}", NotificationType.Quest);
400 }
401}
Shopkeepers
Shop System Implementation
Create NPCs that can sell items and manage inventory:
1"comment">// Shop-related methods "keyword">for the NPC "keyword">class
2
3"keyword">void OpenShop()
4{
5 "keyword">if (!isShopkeeper || npcShop == null)
6 {
7 Debug.LogWarning($"{npcName} is not configured as a shopkeeper!");
8 "keyword">return;
9 }
10
11 "comment">// Check "keyword">if player can access shop
12 "keyword">if (!CanAccessShop())
13 {
14 ShowShopAccessDeniedMessage();
15 "keyword">return;
16 }
17
18 "comment">// Play shop opening effects
19 PlayShopOpenEffects();
20
21 "comment">// Open shop UI
22 "keyword">if (ShopManager.Instance != null)
23 {
24 ShopManager.Instance.OpenShop(npcShop, "keyword">this);
25 }
26
27 "comment">// Trigger event
28 OnShopOpened?.Invoke("keyword">this);
29
30 Debug.Log($"Opened {npcName}'s shop");
31}
32
33"keyword">bool CanAccessShop()
34{
35 "comment">// Check "keyword">if shop is open (time-based)
36 "keyword">if (!IsShopOpen())
37 {
38 "keyword">return false;
39 }
40
41 "comment">// Check relationship requirements
42 "keyword">if (relationshipLevel < -25f)
43 {
44 "keyword">return false; "comment">// Too hostile to trade
45 }
46
47 "comment">// Check quest requirements
48 "keyword">if (npcShop.requiresQuest != null &&
49 !QuestManager.Instance.IsQuestCompleted(npcShop.requiresQuest.questId))
50 {
51 "keyword">return false;
52 }
53
54 "keyword">return true;
55}
56
57"keyword">bool IsShopOpen()
58{
59 "keyword">if (npcShop.operatingHours == null) "keyword">return true;
60
61 "keyword">float currentTime = GetCurrentTimeOfDay();
62 "keyword">return currentTime >= npcShop.operatingHours.openTime &&
63 currentTime <= npcShop.operatingHours.closeTime;
64}
65
66"keyword">void ShowShopAccessDeniedMessage()
67{
68 "keyword">string message = GetShopAccessDeniedReason();
69
70 "keyword">if (UIManager.Instance != null)
71 {
72 UIManager.Instance.ShowNotification(message, NotificationType.Warning);
73 }
74
75 "comment">// Show contextual dialogue
76 ShowCasualMessage(message);
77}
78
79"keyword">string GetShopAccessDeniedReason()
80{
81 "keyword">if (!IsShopOpen())
82 {
83 "keyword">return $"Sorry, my shop is closed right now. Come back during business hours.";
84 }
85
86 "keyword">if (relationshipLevel < -25f)
87 {
88 "keyword">return "I don't want to do business with you right now.";
89 }
90
91 "keyword">if (npcShop.requiresQuest != null &&
92 !QuestManager.Instance.IsQuestCompleted(npcShop.requiresQuest.questId))
93 {
94 "keyword">return $"I can only trade with those who have completed the '{npcShop.requiresQuest.questName}' quest.";
95 }
96
97 "keyword">return "I can't trade with you right now.";
98}
99
100"keyword">void PlayShopOpenEffects()
101{
102 "comment">// Audio
103 AudioManager.Instance?.PlaySFX("ShopOpen", transform);
104
105 "comment">// Animation
106 "keyword">if (animator != null)
107 {
108 animator.SetTrigger("OpenShop");
109 }
110
111 "comment">// Visual effect
112 "keyword">if (UIManager.Instance != null)
113 {
114 UIManager.Instance.PlayShopOpenEffect(transform.position);
115 }
116}
117
118"comment">// Shop inventory management
119"keyword">public "keyword">void RestockShop()
120{
121 "keyword">if (npcShop == null) "keyword">return;
122
123 "keyword">foreach (var shopItem in npcShop.items)
124 {
125 "comment">// Restock items based on restock rules
126 "keyword">if (shopItem.restockAmount > 0)
127 {
128 shopItem.stock += shopItem.restockAmount;
129 shopItem.stock = Mathf.Min(shopItem.stock, shopItem.maxStock);
130 }
131 "keyword">else "keyword">if (shopItem.stock == -1)
132 {
133 "comment">// Infinite stock items don't need restocking
134 continue;
135 }
136 }
137
138 Debug.Log($"{npcName}'s shop restocked");
139}
140
141"keyword">public "keyword">void UpdateShopPrices()
142{
143 "keyword">if (npcShop == null) "keyword">return;
144
145 "comment">// Apply dynamic pricing based on various factors
146 "keyword">float priceModifier = CalculatePriceModifier();
147
148 "keyword">foreach (var shopItem in npcShop.items)
149 {
150 shopItem.currentPrice = Mathf.RoundToInt(shopItem.basePrice * priceModifier);
151 }
152}
153
154"keyword">float CalculatePriceModifier()
155{
156 "keyword">float modifier = 1f;
157
158 "comment">// Relationship affects prices
159 "keyword">if (relationshipLevel > 50f)
160 {
161 modifier *= 0.9f; "comment">// 10% discount "keyword">for good friends
162 }
163 "keyword">else "keyword">if (relationshipLevel < -25f)
164 {
165 modifier *= 1.2f; "comment">// 20% markup "keyword">for disliked players
166 }
167
168 "comment">// Supply and demand (simplified)
169 modifier *= Random.Range(0.85f, 1.15f);
170
171 "comment">// Special events or quests could affect prices
172 "keyword">if (IsSpecialEvent())
173 {
174 modifier *= 0.8f; "comment">// Sale event
175 }
176
177 "keyword">return modifier;
178}
179
180"keyword">bool IsSpecialEvent()
181{
182 "comment">// Check "keyword">for special events that affect shop prices
183 "keyword">return GameManager.Instance.IsEventActive("MerchantSale");
184}
185
186"comment">// Shop dialogue integration
187"keyword">string GetShopDialogue()
188{
189 "keyword">if (!CanAccessShop())
190 {
191 "keyword">return GetShopAccessDeniedReason();
192 }
193
194 "keyword">string[] shopGreetings = {
195 "Welcome to my shop! Take a look around.",
196 "What can I get ">for you today?",
197 "I've got some fine wares ">for sale!",
198 "Feel free to browse my selection."
199 };
200
201 "comment">// Modify greeting based on relationship
202 "keyword">if (relationshipLevel > 75f)
203 {
204 "keyword">return "Ah, my best customer! I've got something special just ">for you.";
205 }
206 "keyword">else "keyword">if (relationshipLevel < -10f)
207 {
208 "keyword">return "I suppose I can sell you something... ">if you have the coin.";
209 }
210
211 "keyword">return shopGreetings[Random.Range(0, shopGreetings.Length)];
212}
213
214"comment">// Advanced shop features
215"keyword">public "keyword">void AddSpecialOffer(Item item, "keyword">int price, "keyword">int duration)
216{
217 "keyword">if (npcShop == null) "keyword">return;
218
219 var specialOffer = "keyword">new ShopItem
220 {
221 item = item,
222 basePrice = price,
223 currentPrice = price,
224 stock = 1,
225 isSpecialOffer = true,
226 offerEndTime = Time.time + duration
227 };
228
229 npcShop.items.Add(specialOffer);
230
231 "comment">// Notify player of special offer
232 "keyword">if (UIManager.Instance != null)
233 {
234 UIManager.Instance.ShowNotification($"{npcName} has a special offer: {item.itemName}!", NotificationType.Shop);
235 }
236}
237
238"keyword">public "keyword">void ProcessPurchase(ShopItem item, "keyword">int quantity)
239{
240 "comment">// Update stock
241 "keyword">if (item.stock > 0)
242 {
243 item.stock -= quantity;
244
245 "keyword">if (item.stock <= 0 && item.isSpecialOffer)
246 {
247 "comment">// Remove sold out special offers
248 npcShop.items.Remove(item);
249 }
250 }
251
252 "comment">// Update relationship (small positive boost "keyword">for purchases)
253 ModifyRelationship(0.5f);
254
255 "comment">// Play purchase sound
256 AudioManager.Instance?.PlaySFX("Purchase", transform);
257
258 Debug.Log($"Player purchased {quantity} {item.item.itemName} from {npcName}");
259}
260
261"keyword">public "keyword">void ProcessSale(Item item, "keyword">int quantity, "keyword">int totalValue)
262{
263 "comment">// Player is selling items to "keyword">this shop
264
265 "comment">// Check "keyword">if shop buys "keyword">this type of item
266 "keyword">if (!WillBuyItem(item))
267 {
268 "keyword">return;
269 }
270
271 "comment">// Calculate buy-back price (usually lower than sell price)
272 "keyword">float buyBackRatio = 0.6f; "comment">// Shop buys at 60% of item value
273 "keyword">if (relationshipLevel > 50f)
274 {
275 buyBackRatio = 0.75f; "comment">// Better rate "keyword">for friends
276 }
277
278 "keyword">int buyBackPrice = Mathf.RoundToInt(totalValue * buyBackRatio);
279
280 "comment">// Give gold to player
281 InventoryManager.Instance.AddGold(buyBackPrice);
282
283 "comment">// Add to shop inventory "keyword">if there's space
284 AddToShopInventory(item, quantity);
285
286 "comment">// Update relationship
287 ModifyRelationship(1f);
288
289 "comment">// Play sell sound
290 AudioManager.Instance?.PlaySFX("Sell", transform);
291
292 Debug.Log($"Player sold {quantity} {item.itemName} to {npcName} ">for {buyBackPrice} gold");
293}
294
295"keyword">bool WillBuyItem(Item item)
296{
297 "comment">// Check shop's buying preferences
298 "keyword">if (npcShop.buyItemTypes == null) "keyword">return true;
299
300 "keyword">return System.Array.Exists(npcShop.buyItemTypes, type => type == item.type);
301}
302
303"keyword">void AddToShopInventory(Item item, "keyword">int quantity)
304{
305 "comment">// Find existing shop item
306 var existingItem = npcShop.items.Find(si => si.item == item);
307
308 "keyword">if (existingItem != null && existingItem.stock >= 0)
309 {
310 existingItem.stock += quantity;
311 }
312 "keyword">else
313 {
314 "comment">// Add as "keyword">new shop item
315 var newShopItem = "keyword">new ShopItem
316 {
317 item = item,
318 basePrice = item.value,
319 currentPrice = item.value,
320 stock = quantity
321 };
322 npcShop.items.Add(newShopItem);
323 }
324}
Shop Data Structure
Create the supporting data structures for the shop system:
1[CreateAssetMenu(fileName = "New Shop", menuName = "RPG/Shop")]
2"keyword">public "keyword">class Shop : ScriptableObject
3{
4 [Header("Shop Info")]
5 "keyword">public "keyword">string shopName;
6 "keyword">public "keyword">string shopkeeper;
7 [TextArea(2, 4)]
8 "keyword">public "keyword">string description;
9
10 [Header("Operating Hours")]
11 "keyword">public ShopHours operatingHours;
12
13 [Header("Access Requirements")]
14 "keyword">public Quest requiresQuest;
15 "keyword">public "keyword">int minimumLevel = 1;
16 "keyword">public "keyword">float minimumRelationship = -100f;
17
18 [Header("Shop Items")]
19 "keyword">public List<ShopItem> items = "keyword">new List<ShopItem>();
20
21 [Header("Buying Preferences")]
22 "keyword">public ItemType[] buyItemTypes;
23 "keyword">public "keyword">float buyBackRatio = 0.6f;
24
25 [Header("Restock Settings")]
26 "keyword">public "keyword">int restockHours = 24;
27 "keyword">public "keyword">bool autoRestock = true;
28
29 "keyword">private "keyword">float lastRestockTime;
30
31 "keyword">public "keyword">bool NeedsRestock()
32 {
33 "keyword">return autoRestock && (Time.time - lastRestockTime) >= (restockHours * 3600f);
34 }
35
36 "keyword">public "keyword">void MarkRestocked()
37 {
38 lastRestockTime = Time.time;
39 }
40}
41
42[System.Serializable]
43"keyword">public "keyword">class ShopItem
44{
45 "keyword">public Item item;
46 "keyword">public "keyword">int basePrice;
47 "keyword">public "keyword">int currentPrice;
48 "keyword">public "keyword">int stock = -1; "comment">// -1 means infinite
49 "keyword">public "keyword">int maxStock = 99;
50 "keyword">public "keyword">int restockAmount = 0;
51 "keyword">public "keyword">bool available = true;
52 "keyword">public "keyword">bool isSpecialOffer = false;
53 "keyword">public "keyword">float offerEndTime;
54
55 "keyword">public "keyword">bool IsAvailable()
56 {
57 "keyword">if (!available) "keyword">return false;
58 "keyword">if (stock == 0) "keyword">return false;
59 "keyword">if (isSpecialOffer && Time.time > offerEndTime) "keyword">return false;
60 "keyword">return true;
61 }
62
63 "keyword">public "keyword">bool HasStock("keyword">int requestedAmount)
64 {
65 "keyword">return stock == -1 || stock >= requestedAmount;
66 }
67}
68
69[System.Serializable]
70"keyword">public "keyword">class ShopHours
71{
72 "keyword">public "keyword">float openTime = 8f; "comment">// 8 AM
73 "keyword">public "keyword">float closeTime = 18f; "comment">// 6 PM
74 "keyword">public "keyword">bool alwaysOpen = false;
75 "keyword">public DayOfWeek[] closedDays;
76
77 "keyword">public "keyword">bool IsOpen("keyword">float currentTime, DayOfWeek currentDay)
78 {
79 "keyword">if (alwaysOpen) "keyword">return true;
80
81 "comment">// Check "keyword">if closed today
82 "keyword">if (closedDays != null && System.Array.Exists(closedDays, day => day == currentDay))
83 {
84 "keyword">return false;
85 }
86
87 "comment">// Check time
88 "keyword">return currentTime >= openTime && currentTime <= closeTime;
89 }
90}
Interactive NPCs
Special Interaction Types
Create NPCs with unique interaction mechanics:
1"comment">// Add these specialized interaction methods to the NPC "keyword">class
2
3[Header("Special Interactions")]
4"keyword">public NPCService[] services;
5"keyword">public MiniGame[] miniGames;
6"keyword">public CraftingRecipe[] teachableRecipes;
7
8[System.Serializable]
9"keyword">public "keyword">class NPCService
10{
11 "keyword">public "keyword">string serviceName;
12 "keyword">public ServiceType type;
13 "keyword">public "keyword">int cost;
14 "keyword">public "keyword">string description;
15 "keyword">public "keyword">bool available = true;
16}
17
18"keyword">public "keyword">enum ServiceType
19{
20 Healing,
21 Repair,
22 Training,
23 Information,
24 Transportation,
25 Storage,
26 Custom
27}
28
29"keyword">void HandleSpecialInteraction()
30{
31 "comment">// Check "keyword">for available services
32 "keyword">if (services != null && services.Length > 0)
33 {
34 ShowServiceMenu();
35 "keyword">return;
36 }
37
38 "comment">// Check "keyword">for mini-games
39 "keyword">if (miniGames != null && miniGames.Length > 0)
40 {
41 StartMiniGame();
42 "keyword">return;
43 }
44
45 "comment">// Check "keyword">for teachable recipes
46 "keyword">if (teachableRecipes != null && teachableRecipes.Length > 0)
47 {
48 OfferRecipeTeaching();
49 "keyword">return;
50 }
51
52 "comment">// Default to normal interaction
53 HandleInteraction();
54}
55
56"keyword">void ShowServiceMenu()
57{
58 var availableServices = System.Array.FindAll(services, s => s.available);
59
60 "keyword">if (availableServices.Length == 0)
61 {
62 ShowCasualMessage("I don't have any services available right now.");
63 "keyword">return;
64 }
65
66 "comment">// Show service selection UI
67 "keyword">if (UIManager.Instance != null)
68 {
69 UIManager.Instance.ShowServiceMenu("keyword">this, availableServices);
70 }
71}
72
73"keyword">public "keyword">void ProvideService(NPCService service)
74{
75 "keyword">if (!service.available) "keyword">return;
76
77 var player = GameManager.Instance.GetPlayer();
78 var playerStats = player.GetComponent<PlayerStats>();
79 var inventory = InventoryManager.Instance;
80
81 "comment">// Check "keyword">if player can afford the service
82 "keyword">if (inventory.GetGold() < service.cost)
83 {
84 ShowCasualMessage("You don't have enough gold ">for that service.");
85 "keyword">return;
86 }
87
88 "comment">// Deduct cost
89 inventory.RemoveGold(service.cost);
90
91 "comment">// Provide the service
92 switch (service.type)
93 {
94 case ServiceType.Healing:
95 ProvideHealing(playerStats);
96 break;
97
98 case ServiceType.Repair:
99 RepairPlayerEquipment();
100 break;
101
102 case ServiceType.Training:
103 ProvideTraining(playerStats);
104 break;
105
106 case ServiceType.Information:
107 ProvideInformation();
108 break;
109
110 case ServiceType.Transportation:
111 ProvideTransportation();
112 break;
113
114 case ServiceType.Storage:
115 OpenStorage();
116 break;
117 }
118
119 "comment">// Play service sound
120 AudioManager.Instance?.PlaySFX($"Service{service.type}", transform);
121
122 "comment">// Update relationship
123 ModifyRelationship(2f);
124
125 Debug.Log($"{npcName} provided {service.serviceName} service");
126}
127
128"keyword">void ProvideHealing(PlayerStats playerStats)
129{
130 "keyword">int healAmount = playerStats.maxHealth - playerStats.currentHealth;
131 playerStats.Heal(healAmount);
132
133 "comment">// Visual effect
134 "keyword">if (UIManager.Instance != null)
135 {
136 UIManager.Instance.PlayHealingEffect(playerStats.transform.position);
137 }
138
139 ShowCasualMessage("You feel much better now!");
140}
141
142"keyword">void RepairPlayerEquipment()
143{
144 var equipment = InventoryManager.Instance.GetEquippedItems();
145
146 "keyword">foreach (var item in equipment)
147 {
148 "keyword">if (item is Equipment equip && equip.durability < equip.maxDurability)
149 {
150 equip.durability = equip.maxDurability;
151 }
152 }
153
154 ShowCasualMessage("Your equipment has been fully repaired!");
155}
156
157"keyword">void ProvideTraining(PlayerStats playerStats)
158{
159 "comment">// Temporary stat boost or skill training
160 playerStats.AddTemporaryBonus("NPCTraining", StatType.Strength, 2, 3600f); "comment">// 1 hour boost
161
162 ShowCasualMessage("I've taught you some ">new techniques. You feel stronger!");
163}
164
165"keyword">void ProvideInformation()
166{
167 "comment">// Give hints about quests, locations, or secrets
168 "keyword">string[] infoMessages = {
169 "I've heard there's treasure hidden in the old ruins to the north.",
170 "The merchant in the next town pays extra ">for rare gems.",
171 "Beware of the wolves that come out at night near the forest.",
172 "There's a secret passage behind the waterfall."
173 };
174
175 "keyword">string info = infoMessages[Random.Range(0, infoMessages.Length)];
176 ShowCasualMessage(info);
177}
178
179"keyword">void ProvideTransportation()
180{
181 "comment">// Fast travel or mount services
182 "keyword">if (TransportationManager.Instance != null)
183 {
184 TransportationManager.Instance.ShowTransportOptions("keyword">this);
185 }
186}
187
188"keyword">void OpenStorage()
189{
190 "comment">// Personal storage service
191 "keyword">if (StorageManager.Instance != null)
192 {
193 StorageManager.Instance.OpenNPCStorage(npcName);
194 }
195}
196
197"comment">// Mini-game integration
198"keyword">void StartMiniGame()
199{
200 "keyword">if (miniGames.Length == 0) "keyword">return;
201
202 var availableGames = System.Array.FindAll(miniGames, g => g.isAvailable);
203 "keyword">if (availableGames.Length == 0)
204 {
205 ShowCasualMessage("I don't have any games to play right now.");
206 "keyword">return;
207 }
208
209 "comment">// Select random mini-game or show selection
210 var selectedGame = availableGames[Random.Range(0, availableGames.Length)];
211
212 "keyword">if (MiniGameManager.Instance != null)
213 {
214 MiniGameManager.Instance.StartMiniGame(selectedGame, "keyword">this);
215 }
216}
217
218"keyword">public "keyword">void OnMiniGameCompleted(MiniGame game, "keyword">bool won, "keyword">int score)
219{
220 "keyword">if (won)
221 {
222 "comment">// Give reward
223 GiveMiniGameReward(game, score);
224 ShowCasualMessage("Well played! Here's your reward.");
225 }
226 "keyword">else
227 {
228 ShowCasualMessage("Better luck next time!");
229 }
230
231 "comment">// Update relationship based on outcome
232 ModifyRelationship(won ? 3f : 1f);
233}
234
235"keyword">void GiveMiniGameReward(MiniGame game, "keyword">int score)
236{
237 "comment">// Calculate reward based on score
238 "keyword">int goldReward = Mathf.RoundToInt(score * game.goldMultiplier);
239 "keyword">int expReward = Mathf.RoundToInt(score * game.expMultiplier);
240
241 InventoryManager.Instance.AddGold(goldReward);
242 GameManager.Instance.GetPlayer().GetComponent<PlayerStats>().GainExperience(expReward);
243
244 "comment">// Chance "keyword">for special item reward
245 "keyword">if (game.possibleRewards != null && Random.value < game.rewardChance)
246 {
247 var reward = game.possibleRewards[Random.Range(0, game.possibleRewards.Length)];
248 InventoryManager.Instance.AddItem(reward);
249 ShowCasualMessage($"You also earned a {reward.itemName}!");
250 }
251}
252
253"comment">// Recipe teaching
254"keyword">void OfferRecipeTeaching()
255{
256 var availableRecipes = GetTeachableRecipes();
257 "keyword">if (availableRecipes.Length == 0)
258 {
259 ShowCasualMessage("I don't have any ">new recipes to teach you right now.");
260 "keyword">return;
261 }
262
263 "comment">// Show recipe learning UI
264 "keyword">if (UIManager.Instance != null)
265 {
266 UIManager.Instance.ShowRecipeLearningMenu("keyword">this, availableRecipes);
267 }
268}
269
270CraftingRecipe[] GetTeachableRecipes()
271{
272 var craftingSystem = CraftingManager.Instance;
273 "keyword">if (craftingSystem == null) "keyword">return "keyword">new CraftingRecipe[0];
274
275 var knownRecipes = craftingSystem.GetKnownRecipes();
276 "keyword">return System.Array.FindAll(teachableRecipes,
277 recipe => !System.Array.Exists(knownRecipes, known => known == recipe));
278}
279
280"keyword">public "keyword">void TeachRecipe(CraftingRecipe recipe)
281{
282 var craftingSystem = CraftingManager.Instance;
283 "keyword">if (craftingSystem == null) "keyword">return;
284
285 "comment">// Check "keyword">if player meets requirements
286 "keyword">if (!CanLearnRecipe(recipe))
287 {
288 ShowCasualMessage("You don't meet the requirements to learn ">this recipe yet.");
289 "keyword">return;
290 }
291
292 "comment">// Check cost
293 "keyword">if (InventoryManager.Instance.GetGold() < recipe.teachingCost)
294 {
295 ShowCasualMessage("You don't have enough gold to learn ">this recipe.");
296 "keyword">return;
297 }
298
299 "comment">// Deduct cost
300 InventoryManager.Instance.RemoveGold(recipe.teachingCost);
301
302 "comment">// Teach recipe
303 craftingSystem.LearnRecipe(recipe);
304
305 ShowCasualMessage($"I've taught you how to craft {recipe.resultItem.itemName}!");
306
307 "comment">// Update relationship
308 ModifyRelationship(5f);
309
310 Debug.log($"{npcName} taught recipe: {recipe.recipeName}");
311}
312
313"keyword">bool CanLearnRecipe(CraftingRecipe recipe)
314{
315 var playerStats = GameManager.Instance.GetPlayer().GetComponent<PlayerStats>();
316
317 "comment">// Check level requirement
318 "keyword">if (playerStats.level < recipe.levelRequirement)
319 {
320 "keyword">return false;
321 }
322
323 "comment">// Check skill requirements
324 "keyword">if (recipe.skillRequirements != null)
325 {
326 "keyword">foreach (var skillReq in recipe.skillRequirements)
327 {
328 "keyword">if (playerStats.GetSkillLevel(skillReq.skill) < skillReq.level)
329 {
330 "keyword">return false;
331 }
332 }
333 }
334
335 "keyword">return true;
336}
337
338"comment">// Context-sensitive messages
339"keyword">void ShowCasualMessage("keyword">string message)
340{
341 "keyword">if (UIManager.Instance != null)
342 {
343 UIManager.Instance.ShowDialogueBox(npcName, message);
344 }
345
346 Debug.Log($"{npcName}: {message}");
347}
NPC AI Behavior
Movement and Patrol System
Implement AI behavior for autonomous NPC movement:
1"comment">// Add these AI behavior methods to the NPC "keyword">class
2
3IEnumerator PatrolBehavior()
4{
5 "keyword">if (patrolPoints == null || patrolPoints.Length == 0) yield break;
6
7 "keyword">while (allowMovement)
8 {
9 "comment">// Move to current patrol point
10 yield "keyword">return StartCoroutine(MoveToPoint(patrolPoints[currentPatrolIndex].position));
11
12 "comment">// Wait at patrol point
13 yield "keyword">return "keyword">new WaitForSeconds(waitTime + Random.Range(-0.5f, 0.5f));
14
15 "comment">// Move to next patrol point
16 currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
17 }
18}
19
20IEnumerator MoveToPoint(Vector3 targetPosition)
21{
22 Vector3 startPosition = transform.position;
23 "keyword">float journeyLength = Vector3.Distance(startPosition, targetPosition);
24 "keyword">float journeyTime = journeyLength / moveSpeed;
25 "keyword">float elapsedTime = 0;
26
27 "comment">// Face movement direction
28 FaceDirection(targetPosition - startPosition);
29
30 "comment">// Update animation
31 "keyword">if (animator != null)
32 {
33 animator.SetBool("IsMoving", true);
34 }
35
36 "keyword">while (elapsedTime < journeyTime)
37 {
38 elapsedTime += Time.deltaTime;
39 "keyword">float fractionOfJourney = elapsedTime / journeyTime;
40
41 transform.position = Vector3.Lerp(startPosition, targetPosition, fractionOfJourney);
42
43 yield "keyword">return null;
44 }
45
46 transform.position = targetPosition;
47
48 "comment">// Stop animation
49 "keyword">if (animator != null)
50 {
51 animator.SetBool("IsMoving", false);
52 }
53}
54
55"keyword">void FaceDirection(Vector3 direction)
56{
57 "keyword">if (spriteRenderer == null) "keyword">return;
58
59 "comment">// Flip sprite based on movement direction
60 "keyword">if (direction.x < -0.1f)
61 {
62 spriteRenderer.flipX = true;
63 }
64 "keyword">else "keyword">if (direction.x > 0.1f)
65 {
66 spriteRenderer.flipX = false;
67 }
68
69 "comment">// Update animator with direction
70 "keyword">if (animator != null)
71 {
72 animator.SetFloat("MoveX", direction.x);
73 animator.SetFloat("MoveY", direction.y);
74 }
75}
76
77"comment">// Schedule-based behavior
78[System.Serializable]
79"keyword">public "keyword">class NPCSchedule
80{
81 "keyword">public "keyword">string activityName;
82 "keyword">public "keyword">float startTime; "comment">// Hour of day (0-24)
83 "keyword">public "keyword">float endTime;
84 "keyword">public Vector3 targetPosition;
85 "keyword">public "keyword">string animation;
86 "keyword">public "keyword">bool interactable = true;
87}
88
89[Header("AI Schedule")]
90"keyword">public NPCSchedule[] dailySchedule;
91"keyword">public "keyword">bool followSchedule = false;
92
93"keyword">void Update()
94{
95 "keyword">base.Update(); "comment">// Call parent Update
96
97 "keyword">if (followSchedule)
98 {
99 UpdateScheduleBehavior();
100 }
101
102 UpdateEmotionalState();
103}
104
105"keyword">void UpdateScheduleBehavior()
106{
107 "keyword">if (dailySchedule == null || dailySchedule.Length == 0) "keyword">return;
108
109 "keyword">float currentTime = GetCurrentTimeOfDay();
110 NPCSchedule currentActivity = GetCurrentActivity(currentTime);
111
112 "keyword">if (currentActivity != null && !IsAtScheduledLocation(currentActivity))
113 {
114 "comment">// Move to scheduled location
115 StartCoroutine(MoveToScheduledActivity(currentActivity));
116 }
117}
118
119NPCSchedule GetCurrentActivity("keyword">float currentTime)
120{
121 "keyword">foreach (var activity in dailySchedule)
122 {
123 "keyword">if (IsTimeInRange(currentTime, activity.startTime, activity.endTime))
124 {
125 "keyword">return activity;
126 }
127 }
128
129 "keyword">return null; "comment">// Free time
130}
131
132"keyword">bool IsTimeInRange("keyword">float time, "keyword">float start, "keyword">float end)
133{
134 "keyword">if (start <= end)
135 {
136 "keyword">return time >= start && time <= end;
137 }
138 "keyword">else
139 {
140 "comment">// Activity spans midnight
141 "keyword">return time >= start || time <= end;
142 }
143}
144
145"keyword">bool IsAtScheduledLocation(NPCSchedule activity)
146{
147 "keyword">return Vector3.Distance(transform.position, activity.targetPosition) < 0.5f;
148}
149
150IEnumerator MoveToScheduledActivity(NPCSchedule activity)
151{
152 "comment">// Stop current patrol
153 "keyword">if (movementCoroutine != null)
154 {
155 StopCoroutine(movementCoroutine);
156 movementCoroutine = null;
157 }
158
159 "comment">// Move to activity location
160 yield "keyword">return StartCoroutine(MoveToPoint(activity.targetPosition));
161
162 "comment">// Start activity animation
163 "keyword">if (!"keyword">string.IsNullOrEmpty(activity.animation) && animator != null)
164 {
165 animator.SetTrigger(activity.animation);
166 }
167
168 "comment">// Update interactability
169 allowInteraction = activity.interactable;
170
171 Debug.Log($"{npcName} started activity: {activity.activityName}");
172}
173
174"comment">// Emotional state system
175[System.Serializable]
176"keyword">public "keyword">class EmotionalState
177{
178 "keyword">public "keyword">float happiness = 50f;
179 "keyword">public "keyword">float anger = 0f;
180 "keyword">public "keyword">float fear = 0f;
181 "keyword">public "keyword">float trust = 50f;
182
183 "keyword">public NPCMood GetCurrentMood()
184 {
185 "keyword">if (happiness > 70f) "keyword">return NPCMood.Happy;
186 "keyword">if (anger > 60f) "keyword">return NPCMood.Angry;
187 "keyword">if (fear > 50f) "keyword">return NPCMood.Afraid;
188 "keyword">if (trust < 20f) "keyword">return NPCMood.Suspicious;
189 "keyword">if (happiness < 30f) "keyword">return NPCMood.Sad;
190
191 "keyword">return NPCMood.Neutral;
192 }
193}
194
195"keyword">public "keyword">enum NPCMood
196{
197 Happy,
198 Sad,
199 Angry,
200 Afraid,
201 Suspicious,
202 Excited,
203 Bored,
204 Neutral
205}
206
207[Header("Emotional AI")]
208"keyword">public EmotionalState emotionalState;
209"keyword">public "keyword">float emotionDecayRate = 1f;
210
211"keyword">void UpdateEmotionalState()
212{
213 "keyword">if (emotionalState == null) "keyword">return;
214
215 "comment">// Gradual emotion decay toward neutral
216 "keyword">float deltaTime = Time.deltaTime * emotionDecayRate;
217
218 emotionalState.happiness = Mathf.MoveTowards(emotionalState.happiness, 50f, deltaTime);
219 emotionalState.anger = Mathf.MoveTowards(emotionalState.anger, 0f, deltaTime);
220 emotionalState.fear = Mathf.MoveTowards(emotionalState.fear, 0f, deltaTime);
221 emotionalState.trust = Mathf.MoveTowards(emotionalState.trust, 50f, deltaTime * 0.5f);
222
223 "comment">// Update visual representation
224 UpdateEmotionalVisuals();
225}
226
227"keyword">void UpdateEmotionalVisuals()
228{
229 NPCMood currentMood = emotionalState.GetCurrentMood();
230
231 "keyword">if (animator != null)
232 {
233 animator.SetInteger("Mood", ("keyword">int)currentMood);
234 }
235
236 "comment">// Change sprite tint based on mood
237 "keyword">if (spriteRenderer != null)
238 {
239 Color moodColor = GetMoodColor(currentMood);
240 spriteRenderer.color = Color.Lerp(Color.white, moodColor, 0.3f);
241 }
242}
243
244Color GetMoodColor(NPCMood mood)
245{
246 switch (mood)
247 {
248 case NPCMood.Happy: "keyword">return Color.yellow;
249 case NPCMood.Angry: "keyword">return Color.red;
250 case NPCMood.Afraid: "keyword">return Color.blue;
251 case NPCMood.Sad: "keyword">return Color.gray;
252 case NPCMood.Suspicious: "keyword">return Color.magenta;
253 case NPCMood.Excited: "keyword">return Color.cyan;
254 default: "keyword">return Color.white;
255 }
256}
257
258"comment">// Event reactions
259"keyword">public "keyword">void ReactToEvent(GameEvent gameEvent)
260{
261 switch (gameEvent.eventType)
262 {
263 case GameEventType.PlayerDamaged:
264 "keyword">if (npcType == NPCType.Guard)
265 {
266 emotionalState.anger += 10f;
267 ShowCasualMessage("Who dares attack in my presence?!");
268 }
269 break;
270
271 case GameEventType.Combat:
272 emotionalState.fear += 15f;
273 "keyword">if (emotionalState.fear > 70f)
274 {
275 "comment">// Run away from combat
276 StartCoroutine(FleeFromDanger());
277 }
278 break;
279
280 case GameEventType.QuestCompleted:
281 "keyword">if (HasActiveQuestForThisNPC())
282 {
283 emotionalState.happiness += 20f;
284 ShowCasualMessage("Excellent work, adventurer!");
285 }
286 break;
287
288 case GameEventType.PlayerLevelUp:
289 emotionalState.happiness += 5f;
290 ShowCasualMessage("Congratulations on your progress!");
291 break;
292 }
293}
294
295IEnumerator FleeFromDanger()
296{
297 "comment">// Find safe location (away from player/combat)
298 Vector3 safePosition = FindSafePosition();
299
300 "comment">// Increase movement speed temporarily
301 "keyword">float originalSpeed = moveSpeed;
302 moveSpeed *= 2f;
303
304 yield "keyword">return StartCoroutine(MoveToPoint(safePosition));
305
306 "comment">// Restore normal speed
307 moveSpeed = originalSpeed;
308
309 "comment">// Cower animation
310 "keyword">if (animator != null)
311 {
312 animator.SetTrigger("Cower");
313 }
314
315 ShowCasualMessage("Please don't hurt me!");
316}
317
318Vector3 FindSafePosition()
319{
320 "comment">// Simple implementation: move away from player
321 var player = GameManager.Instance.GetPlayer();
322 "keyword">if (player == null) "keyword">return transform.position;
323
324 Vector3 directionAway = (transform.position - player.transform.position).normalized;
325 "keyword">return transform.position + directionAway * 5f;
326}
327
328"comment">// Social interactions with other NPCs
329"keyword">public "keyword">void InteractWithNPC(NPC otherNPC)
330{
331 "keyword">if (otherNPC == null || otherNPC == "keyword">this) "keyword">return;
332
333 "comment">// Determine relationship with other NPC
334 "keyword">float relationship = GetNPCRelationship(otherNPC);
335
336 "keyword">if (relationship > 50f)
337 {
338 "comment">// Friendly interaction
339 StartCoroutine(FriendlyChat(otherNPC));
340 }
341 "keyword">else "keyword">if (relationship < -25f)
342 {
343 "comment">// Hostile interaction
344 StartCoroutine(HostileEncounter(otherNPC));
345 }
346 "keyword">else
347 {
348 "comment">// Neutral greeting
349 StartCoroutine(NeutralGreeting(otherNPC));
350 }
351}
352
353"keyword">float GetNPCRelationship(NPC otherNPC)
354{
355 "comment">// Simple relationship based on NPC types and proximity
356 "keyword">float baseRelationship = 0f;
357
358 "comment">// Same type NPCs tend to get along
359 "keyword">if (npcType == otherNPC.npcType)
360 {
361 baseRelationship += 25f;
362 }
363
364 "comment">// Guards don't like thieves, etc.
365 "keyword">if (npcType == NPCType.Guard && otherNPC.npcType == NPCType.Custom)
366 {
367 baseRelationship -= 30f;
368 }
369
370 "comment">// Add some randomness
371 baseRelationship += Random.Range(-10f, 10f);
372
373 "keyword">return Mathf.Clamp(baseRelationship, -100f, 100f);
374}
375
376IEnumerator FriendlyChat(NPC otherNPC)
377{
378 "comment">// Face each other
379 FaceNPC(otherNPC);
380 otherNPC.FaceNPC("keyword">this);
381
382 "comment">// Chat animation
383 "keyword">if (animator != null)
384 {
385 animator.SetTrigger("Chat");
386 }
387
388 yield "keyword">return "keyword">new WaitForSeconds(2f);
389
390 "comment">// Exchange pleasantries
391 ShowCasualMessage($"Good to see you, {otherNPC.npcName}!");
392
393 yield "keyword">return "keyword">new WaitForSeconds(1f);
394
395 otherNPC.ShowCasualMessage($"Likewise, {npcName}!");
396
397 "comment">// Boost happiness
398 emotionalState.happiness += 5f;
399 otherNPC.emotionalState.happiness += 5f;
400}
401
402IEnumerator HostileEncounter(NPC otherNPC)
403{
404 "comment">// Face each other aggressively
405 FaceNPC(otherNPC);
406 otherNPC.FaceNPC("keyword">this);
407
408 "comment">// Angry animation
409 "keyword">if (animator != null)
410 {
411 animator.SetTrigger("Angry");
412 }
413
414 yield "keyword">return "keyword">new WaitForSeconds(1f);
415
416 ShowCasualMessage($"I don't want to talk to you, {otherNPC.npcName}.");
417
418 "comment">// Increase anger
419 emotionalState.anger += 10f;
420 otherNPC.emotionalState.anger += 5f;
421}
422
423IEnumerator NeutralGreeting(NPC otherNPC)
424{
425 "comment">// Brief acknowledgment
426 FaceNPC(otherNPC);
427
428 "keyword">if (animator != null)
429 {
430 animator.SetTrigger("Nod");
431 }
432
433 yield "keyword">return "keyword">new WaitForSeconds(0.5f);
434
435 "comment">// Continue with normal behavior
436}
437
438"keyword">void FaceNPC(NPC targetNPC)
439{
440 Vector3 direction = targetNPC.transform.position - transform.position;
441 FaceDirection(direction);
442}
443
444"comment">// Memory system
445[System.Serializable]
446"keyword">public "keyword">class NPCMemory
447{
448 "keyword">public "keyword">string eventDescription;
449 "keyword">public "keyword">float timestamp;
450 "keyword">public MemoryType type;
451 "keyword">public "keyword">float emotionalImpact;
452}
453
454"keyword">public "keyword">enum MemoryType
455{
456 Interaction,
457 Combat,
458 Gift,
459 Betrayal,
460 Help,
461 Insult
462}
463
464[Header("Memory System")]
465"keyword">public List<NPCMemory> memories = "keyword">new List<NPCMemory>();
466"keyword">public "keyword">int maxMemories = 20;
467
468"keyword">public "keyword">void RememberEvent("keyword">string description, MemoryType type, "keyword">float impact)
469{
470 var memory = "keyword">new NPCMemory
471 {
472 eventDescription = description,
473 timestamp = Time.time,
474 type = type,
475 emotionalImpact = impact
476 };
477
478 memories.Add(memory);
479
480 "comment">// Remove oldest memories "keyword">if at capacity
481 "keyword">if (memories.Count > maxMemories)
482 {
483 memories.RemoveAt(0);
484 }
485
486 "comment">// Apply emotional impact
487 ApplyMemoryImpact(type, impact);
488}
489
490"keyword">void ApplyMemoryImpact(MemoryType type, "keyword">float impact)
491{
492 switch (type)
493 {
494 case MemoryType.Gift:
495 emotionalState.happiness += impact;
496 relationshipLevel += impact * 0.5f;
497 break;
498
499 case MemoryType.Help:
500 emotionalState.trust += impact;
501 relationshipLevel += impact * 0.3f;
502 break;
503
504 case MemoryType.Betrayal:
505 emotionalState.trust -= impact;
506 emotionalState.anger += impact * 0.5f;
507 relationshipLevel -= impact;
508 break;
509
510 case MemoryType.Insult:
511 emotionalState.anger += impact;
512 relationshipLevel -= impact * 0.2f;
513 break;
514 }
515}
516
517"keyword">public "keyword">bool RemembersEvent(MemoryType type, "keyword">float withinHours = 24f)
518{
519 "keyword">float timeThreshold = Time.time - (withinHours * 3600f);
520
521 "keyword">return memories.Exists(m => m.type == type && m.timestamp > timeThreshold);
522}
523
524"comment">// Context awareness
525"keyword">public "keyword">string GetContextualResponse()
526{
527 "comment">// Check recent memories
528 "keyword">if (RemembersEvent(MemoryType.Gift, 2f))
529 {
530 "keyword">return "Thank you again ">for your generosity!";
531 }
532
533 "keyword">if (RemembersEvent(MemoryType.Betrayal, 48f))
534 {
535 "keyword">return "I still don't trust you after what happened.";
536 }
537
538 "comment">// Check current emotional state
539 NPCMood mood = emotionalState.GetCurrentMood();
540 switch (mood)
541 {
542 case NPCMood.Happy:
543 "keyword">return "I'm having such a wonderful day!";
544 case NPCMood.Angry:
545 "keyword">return "I'm not in the mood to talk right now.";
546 case NPCMood.Afraid:
547 "keyword">return "Is it safe to talk here?";
548 case NPCMood.Sad:
549 "keyword">return "I've been feeling down lately...";
550 }
551
552 "keyword">return GetContextualDialogue();
553}
Advanced NPC Features
NPC Reputation System
Implement a reputation system that affects all NPC interactions:
1"keyword">public "keyword">class NPCReputationSystem : MonoBehaviour
2{
3 "keyword">public "keyword">static NPCReputationSystem Instance { get; "keyword">private set; }
4
5 [System.Serializable]
6 "keyword">public "keyword">class ReputationFaction
7 {
8 "keyword">public "keyword">string factionName;
9 "keyword">public "keyword">float reputation = 0f;
10 "keyword">public "keyword">string description;
11 "keyword">public Color factionColor = Color.white;
12 }
13
14 [Header("Reputation Factions")]
15 "keyword">public List<ReputationFaction> factions = "keyword">new List<ReputationFaction>();
16
17 [Header("Reputation Effects")]
18 "keyword">public "keyword">float maxReputation = 100f;
19 "keyword">public "keyword">float minReputation = -100f;
20
21 "keyword">private Dictionary<"keyword">string, "keyword">float> reputationValues = "keyword">new Dictionary<"keyword">string, "keyword">float>();
22
23 "comment">// Events
24 "keyword">public System.Action<"keyword">string, "keyword">float> OnReputationChanged;
25
26 "keyword">void Awake()
27 {
28 "keyword">if (Instance == null)
29 {
30 Instance = "keyword">this;
31 DontDestroyOnLoad(gameObject);
32 InitializeReputations();
33 }
34 "keyword">else
35 {
36 Destroy(gameObject);
37 }
38 }
39
40 "keyword">void InitializeReputations()
41 {
42 "keyword">foreach (var faction in factions)
43 {
44 reputationValues[faction.factionName] = faction.reputation;
45 }
46 }
47
48 "keyword">public "keyword">void ModifyReputation("keyword">string factionName, "keyword">float change)
49 {
50 "keyword">if (!reputationValues.ContainsKey(factionName))
51 {
52 reputationValues[factionName] = 0f;
53 }
54
55 "keyword">float oldValue = reputationValues[factionName];
56 reputationValues[factionName] = Mathf.Clamp(
57 reputationValues[factionName] + change,
58 minReputation,
59 maxReputation
60 );
61
62 "keyword">float newValue = reputationValues[factionName];
63
64 "keyword">if (!Mathf.Approximately(oldValue, newValue))
65 {
66 OnReputationChanged?.Invoke(factionName, newValue);
67 CheckReputationMilestones(factionName, oldValue, newValue);
68
69 Debug.Log($"Reputation with {factionName}: {newValue:F1} ({change:+F1})");
70 }
71 }
72
73 "keyword">public "keyword">float GetReputation("keyword">string factionName)
74 {
75 "keyword">return reputationValues.TryGetValue(factionName, out "keyword">float value) ? value : 0f;
76 }
77
78 "keyword">public ReputationLevel GetReputationLevel("keyword">string factionName)
79 {
80 "keyword">float rep = GetReputation(factionName);
81
82 "keyword">if (rep >= 75f) "keyword">return ReputationLevel.Revered;
83 "keyword">if (rep >= 50f) "keyword">return ReputationLevel.Honored;
84 "keyword">if (rep >= 25f) "keyword">return ReputationLevel.Friendly;
85 "keyword">if (rep >= -25f) "keyword">return ReputationLevel.Neutral;
86 "keyword">if (rep >= -50f) "keyword">return ReputationLevel.Unfriendly;
87 "keyword">if (rep >= -75f) "keyword">return ReputationLevel.Hostile;
88 "keyword">return ReputationLevel.Hated;
89 }
90
91 "keyword">void CheckReputationMilestones("keyword">string factionName, "keyword">float oldValue, "keyword">float newValue)
92 {
93 ReputationLevel oldLevel = GetReputationLevelFromValue(oldValue);
94 ReputationLevel newLevel = GetReputationLevelFromValue(newValue);
95
96 "keyword">if (oldLevel != newLevel)
97 {
98 OnReputationLevelChanged(factionName, oldLevel, newLevel);
99 }
100 }
101
102 ReputationLevel GetReputationLevelFromValue("keyword">float value)
103 {
104 "keyword">if (value >= 75f) "keyword">return ReputationLevel.Revered;
105 "keyword">if (value >= 50f) "keyword">return ReputationLevel.Honored;
106 "keyword">if (value >= 25f) "keyword">return ReputationLevel.Friendly;
107 "keyword">if (value >= -25f) "keyword">return ReputationLevel.Neutral;
108 "keyword">if (value >= -50f) "keyword">return ReputationLevel.Unfriendly;
109 "keyword">if (value >= -75f) "keyword">return ReputationLevel.Hostile;
110 "keyword">return ReputationLevel.Hated;
111 }
112
113 "keyword">void OnReputationLevelChanged("keyword">string factionName, ReputationLevel oldLevel, ReputationLevel newLevel)
114 {
115 "comment">// Show reputation change notification
116 "keyword">if (UIManager.Instance != null)
117 {
118 "keyword">string message = $"Reputation with {factionName}: {newLevel}";
119 UIManager.Instance.ShowNotification(message, NotificationType.Reputation);
120 }
121
122 "comment">// Unlock/lock content based on reputation
123 HandleReputationEffects(factionName, newLevel);
124 }
125
126 "keyword">void HandleReputationEffects("keyword">string factionName, ReputationLevel level)
127 {
128 "comment">// Example effects based on reputation level
129 switch (level)
130 {
131 case ReputationLevel.Revered:
132 UnlockFactionBenefits(factionName);
133 break;
134
135 case ReputationLevel.Hostile:
136 ActivateFactionHostility(factionName);
137 break;
138 }
139 }
140
141 "keyword">void UnlockFactionBenefits("keyword">string factionName)
142 {
143 "comment">// Special quests, items, or services become available
144 Debug.Log($"Unlocked special benefits with {factionName}!");
145 }
146
147 "keyword">void ActivateFactionHostility("keyword">string factionName)
148 {
149 "comment">// NPCs of "keyword">this faction become hostile
150 var npcs = FindObjectsOfType<NPC>();
151 "keyword">foreach (var npc in npcs)
152 {
153 "keyword">if (npc.faction == factionName)
154 {
155 npc.SetHostile(true);
156 }
157 }
158 }
159}
160
161"keyword">public "keyword">enum ReputationLevel
162{
163 Hated = -3,
164 Hostile = -2,
165 Unfriendly = -1,
166 Neutral = 0,
167 Friendly = 1,
168 Honored = 2,
169 Revered = 3
170}
171
172"comment">// Add to NPC "keyword">class
173[Header("Faction System")]
174"keyword">public "keyword">string faction = "Neutral";
175"keyword">public "keyword">bool affectedByReputation = true;
176
177"keyword">public "keyword">override "keyword">bool CanInteract()
178{
179 "keyword">if (!"keyword">base.CanInteract()) "keyword">return false;
180
181 "keyword">if (affectedByReputation && !"keyword">string.IsNullOrEmpty(faction))
182 {
183 var reputationLevel = NPCReputationSystem.Instance.GetReputationLevel(faction);
184
185 "comment">// Hostile factions won't interact
186 "keyword">if (reputationLevel <= ReputationLevel.Hostile)
187 {
188 "keyword">return false;
189 }
190 }
191
192 "keyword">return true;
193}
194
195"keyword">public "keyword">void SetHostile("keyword">bool hostile)
196{
197 "keyword">if (hostile)
198 {
199 emotionalState.anger = 100f;
200 allowInteraction = false;
201
202 "comment">// Change to hostile dialogue
203 "keyword">if (spriteRenderer != null)
204 {
205 spriteRenderer.color = Color.red;
206 }
207 }
208 "keyword">else
209 {
210 emotionalState.anger = 0f;
211 allowInteraction = true;
212
213 "keyword">if (spriteRenderer != null)
214 {
215 spriteRenderer.color = Color.white;
216 }
217 }
218}
219
220"keyword">public "keyword">float GetReputationModifier()
221{
222 "keyword">if (!affectedByReputation || "keyword">string.IsNullOrEmpty(faction))
223 "keyword">return 1f;
224
225 var level = NPCReputationSystem.Instance.GetReputationLevel(faction);
226
227 switch (level)
228 {
229 case ReputationLevel.Revered: "keyword">return 0.7f; "comment">// 30% discount
230 case ReputationLevel.Honored: "keyword">return 0.85f; "comment">// 15% discount
231 case ReputationLevel.Friendly: "keyword">return 0.95f; "comment">// 5% discount
232 case ReputationLevel.Neutral: "keyword">return 1f; "comment">// Normal prices
233 case ReputationLevel.Unfriendly: "keyword">return 1.15f; "comment">// 15% markup
234 case ReputationLevel.Hostile: "keyword">return 1.5f; "comment">// 50% markup
235 default: "keyword">return 2f; "comment">// Double prices "keyword">for hated
236 }
237}
Dynamic NPC Generation
Create a system for generating NPCs procedurally:
1"keyword">public "keyword">class NPCGenerator : MonoBehaviour
2{
3 [Header("Generation Settings")]
4 "keyword">public NPCTemplate[] npcTemplates;
5 "keyword">public "keyword">string[] firstNames;
6 "keyword">public "keyword">string[] lastNames;
7 "keyword">public Sprite[] maleSprites;
8 "keyword">public Sprite[] femaleSprites;
9
10 [Header("Dialogue Generation")]
11 "keyword">public "keyword">string[] greetingTemplates;
12 "keyword">public "keyword">string[] farewellTemplates;
13 "keyword">public "keyword">string[] questRefusalLines;
14
15 [System.Serializable]
16 "keyword">public "keyword">class NPCTemplate
17 {
18 "keyword">public NPCType type;
19 "keyword">public "keyword">string[] possibleNames;
20 "keyword">public Vector2 levelRange = "keyword">new Vector2(1, 10);
21 "keyword">public Vector2 relationshipRange = "keyword">new Vector2(-10, 10);
22 "keyword">public "keyword">bool canGiveQuests = false;
23 "keyword">public "keyword">bool canSellItems = false;
24 "keyword">public Quest[] possibleQuests;
25 "keyword">public Item[] possibleShopItems;
26 }
27
28 "keyword">public NPC GenerateRandomNPC(Vector3 position)
29 {
30 "comment">// Create GameObject
31 GameObject npcObject = "keyword">new GameObject("Generated_NPC");
32 npcObject.transform.position = position;
33
34 "comment">// Add components
35 var spriteRenderer = npcObject.AddComponent<SpriteRenderer>();
36 var collider = npcObject.AddComponent<BoxCollider2D>();
37 var audioSource = npcObject.AddComponent<AudioSource>();
38 var npcComponent = npcObject.AddComponent<NPC>();
39
40 collider.isTrigger = true;
41 collider.size = "keyword">new Vector2(1f, 1f);
42
43 "comment">// Generate NPC data
44 PopulateNPCData(npcComponent, spriteRenderer);
45
46 "keyword">return npcComponent;
47 }
48
49 "keyword">void PopulateNPCData(NPC npc, SpriteRenderer renderer)
50 {
51 "comment">// Select random template
52 var template = npcTemplates[Random.Range(0, npcTemplates.Length)];
53
54 "comment">// Generate basic info
55 npc.npcType = template.type;
56 npc.npcName = GenerateName(template);
57 npc.description = GenerateDescription(npc.npcType, npc.npcName);
58
59 "comment">// Generate appearance
60 "keyword">bool isMale = Random.value > 0.5f;
61 renderer.sprite = GetRandomSprite(isMale);
62
63 "comment">// Generate stats
64 npc.relationshipLevel = Random.Range(template.relationshipRange.x, template.relationshipRange.y);
65
66 "comment">// Generate dialogue
67 GenerateDialogue(npc);
68
69 "comment">// Generate quests "keyword">if applicable
70 "keyword">if (template.canGiveQuests && Random.value < 0.3f)
71 {
72 GenerateQuest(npc, template);
73 }
74
75 "comment">// Generate shop "keyword">if applicable
76 "keyword">if (template.canSellItems && Random.value < 0.2f)
77 {
78 GenerateShop(npc, template);
79 }
80
81 "comment">// Generate personality traits
82 GeneratePersonality(npc);
83
84 Debug.Log($"Generated NPC: {npc.npcName} ({npc.npcType})");
85 }
86
87 "keyword">string GenerateName(NPCTemplate template)
88 {
89 "keyword">string firstName;
90
91 "keyword">if (template.possibleNames != null && template.possibleNames.Length > 0)
92 {
93 firstName = template.possibleNames[Random.Range(0, template.possibleNames.Length)];
94 }
95 "keyword">else
96 {
97 firstName = firstNames[Random.Range(0, firstNames.Length)];
98 }
99
100 "comment">// Sometimes add last name
101 "keyword">if (Random.value < 0.7f)
102 {
103 "keyword">string lastName = lastNames[Random.Range(0, lastNames.Length)];
104 "keyword">return $"{firstName} {lastName}";
105 }
106
107 "keyword">return firstName;
108 }
109
110 "keyword">string GenerateDescription(NPCType type, "keyword">string name)
111 {
112 "keyword">string[] descriptions = GetDescriptionTemplates(type);
113 "keyword">string template = descriptions[Random.Range(0, descriptions.Length)];
114 "keyword">return template.Replace("{name}", name);
115 }
116
117 "keyword">string[] GetDescriptionTemplates(NPCType type)
118 {
119 switch (type)
120 {
121 case NPCType.Merchant:
122 "keyword">return "keyword">new "keyword">string[] {
123 "{name} is a traveling merchant with goods from distant lands.",
124 "{name} runs a small but profitable trading business.",
125 "{name} has an eye ">for valuable items and fair deals."
126 };
127
128 case NPCType.Guard:
129 "keyword">return "keyword">new "keyword">string[] {
130 "{name} is a dedicated protector of the community.",
131 "{name} takes their duty seriously and watches ">for trouble.",
132 "{name} has years of experience keeping the peace."
133 };
134
135 case NPCType.Villager:
136 "keyword">return "keyword">new "keyword">string[] {
137 "{name} is a friendly local resident.",
138 "{name} has lived here ">for many years.",
139 "{name} knows everyone in town and all the local gossip."
140 };
141
142 default:
143 "keyword">return "keyword">new "keyword">string[] {
144 "{name} is a notable member of the community.",
145 "{name} has their own unique story to tell."
146 };
147 }
148 }
149
150 Sprite GetRandomSprite("keyword">bool isMale)
151 {
152 var sprites = isMale ? maleSprites : femaleSprites;
153 "keyword">return sprites[Random.Range(0, sprites.Length)];
154 }
155
156 "keyword">void GenerateDialogue(NPC npc)
157 {
158 "comment">// Generate casual greetings
159 npc.casualGreetings = "keyword">new "keyword">string[Random.Range(2, 5)];
160 "keyword">for ("keyword">int i = 0; i < npc.casualGreetings.Length; i++)
161 {
162 npc.casualGreetings[i] = PersonalizeGreeting(
163 greetingTemplates[Random.Range(0, greetingTemplates.Length)],
164 npc
165 );
166 }
167 }
168
169 "keyword">string PersonalizeGreeting("keyword">string template, NPC npc)
170 {
171 template = template.Replace("{name}", npc.npcName);
172 template = template.Replace("{type}", npc.npcType.ToString().ToLower());
173
174 "comment">// Add personality touches based on relationship
175 "keyword">if (npc.relationshipLevel > 25f)
176 {
177 template = "My friend! " + template;
178 }
179 "keyword">else "keyword">if (npc.relationshipLevel < -25f)
180 {
181 template = template.Replace("!", ".");
182 }
183
184 "keyword">return template;
185 }
186
187 "keyword">void GenerateQuest(NPC npc, NPCTemplate template)
188 {
189 "keyword">if (template.possibleQuests == null || template.possibleQuests.Length == 0)
190 "keyword">return;
191
192 var questTemplate = template.possibleQuests[Random.Range(0, template.possibleQuests.Length)];
193
194 "comment">// Create a copy of the quest with personalized details
195 var personalizedQuest = ScriptableObject.Instantiate(questTemplate);
196 personalizedQuest.questGiver = npc.npcName;
197 personalizedQuest.questName = PersonalizeQuestName(questTemplate.questName, npc);
198 personalizedQuest.description = PersonalizeQuestDescription(questTemplate.description, npc);
199
200 npc.questToGive = personalizedQuest;
201 }
202
203 "keyword">void GenerateShop(NPC npc, NPCTemplate template)
204 {
205 "keyword">if (template.possibleShopItems == null || template.possibleShopItems.Length == 0)
206 "keyword">return;
207
208 npc.isShopkeeper = true;
209
210 "comment">// Create shop with random selection of items
211 "keyword">int itemCount = Random.Range(3, 8);
212 npc.shopItems = "keyword">new Item[itemCount];
213
214 "keyword">for ("keyword">int i = 0; i < itemCount; i++)
215 {
216 npc.shopItems[i] = template.possibleShopItems[Random.Range(0, template.possibleShopItems.Length)];
217 }
218 }
219
220 "keyword">void GeneratePersonality(NPC npc)
221 {
222 "comment">// Generate emotional state
223 npc.emotionalState = "keyword">new EmotionalState
224 {
225 happiness = Random.Range(30f, 70f),
226 anger = Random.Range(0f, 20f),
227 fear = Random.Range(0f, 30f),
228 trust = Random.Range(40f, 60f)
229 };
230
231 "comment">// Generate movement behavior
232 "keyword">if (Random.value < 0.7f)
233 {
234 npc.allowMovement = true;
235 npc.moveSpeed = Random.Range(0.5f, 2f);
236 npc.waitTime = Random.Range(1f, 4f);
237 }
238 }
239
240 "keyword">string PersonalizeQuestName("keyword">string template, NPC npc)
241 {
242 "keyword">return template.Replace("{giver}", npc.npcName);
243 }
244
245 "keyword">string PersonalizeQuestDescription("keyword">string template, NPC npc)
246 {
247 "keyword">return template.Replace("{giver}", npc.npcName)
248 .Replace("{location}", GetRandomLocation());
249 }
250
251 "keyword">string GetRandomLocation()
252 {
253 "keyword">string[] locations = { "the forest", "the cave", "the ruins", "the mountain", "the village" };
254 "keyword">return locations[Random.Range(0, locations.Length)];
255 }
256}
NPC Management System
Global NPC Manager
Create a centralized system to manage all NPCs in your game:
1"keyword">public "keyword">class NPCManager : MonoBehaviour
2{
3 "keyword">public "keyword">static NPCManager Instance { get; "keyword">private set; }
4
5 [Header("NPC Management")]
6 "keyword">public List<NPC> allNPCs = "keyword">new List<NPC>();
7 "keyword">public "keyword">float updateInterval = 1f;
8
9 [Header("Save Data")]
10 "keyword">public "keyword">bool saveNPCStates = true;
11
12 "keyword">private Dictionary<"keyword">string, NPCData> npcSaveData = "keyword">new Dictionary<"keyword">string, NPCData>();
13 "keyword">private Coroutine managementCoroutine;
14
15 "keyword">void Awake()
16 {
17 "keyword">if (Instance == null)
18 {
19 Instance = "keyword">this;
20 DontDestroyOnLoad(gameObject);
21 StartManagement();
22 }
23 "keyword">else
24 {
25 Destroy(gameObject);
26 }
27 }
28
29 "keyword">void StartManagement()
30 {
31 "comment">// Find all NPCs in scene
32 RefreshNPCList();
33
34 "comment">// Start management coroutine
35 managementCoroutine = StartCoroutine(ManagementLoop());
36
37 "comment">// Load saved NPC data
38 "keyword">if (saveNPCStates)
39 {
40 LoadNPCData();
41 }
42 }
43
44 "keyword">public "keyword">void RefreshNPCList()
45 {
46 allNPCs.Clear();
47 allNPCs.AddRange(FindObjectsOfType<NPC>());
48 Debug.Log($"Found {allNPCs.Count} NPCs in scene");
49 }
50
51 IEnumerator ManagementLoop()
52 {
53 "keyword">while (true)
54 {
55 yield "keyword">return "keyword">new WaitForSeconds(updateInterval);
56
57 "comment">// Update all NPCs
58 UpdateNPCs();
59
60 "comment">// Perform periodic saves
61 "keyword">if (saveNPCStates && Time.time % 60f < updateInterval)
62 {
63 SaveNPCData();
64 }
65 }
66 }
67
68 "keyword">void UpdateNPCs()
69 {
70 "keyword">foreach (var npc in allNPCs)
71 {
72 "keyword">if (npc == null) continue;
73
74 "comment">// Update NPC schedules
75 "keyword">if (npc.followSchedule)
76 {
77 UpdateNPCSchedule(npc);
78 }
79
80 "comment">// Handle reputation effects
81 "keyword">if (npc.affectedByReputation)
82 {
83 UpdateReputationEffects(npc);
84 }
85
86 "comment">// Process memory decay
87 ProcessMemoryDecay(npc);
88 }
89 }
90
91 "keyword">public "keyword">void RegisterNPC(NPC npc)
92 {
93 "keyword">if (!allNPCs.Contains(npc))
94 {
95 allNPCs.Add(npc);
96
97 "comment">// Apply saved data "keyword">if available
98 "keyword">if (npcSaveData.TryGetValue(npc.npcName, out NPCData data))
99 {
100 ApplyNPCData(npc, data);
101 }
102 }
103 }
104
105 "keyword">public "keyword">void UnregisterNPC(NPC npc)
106 {
107 allNPCs.Remove(npc);
108 }
109
110 "keyword">void SaveNPCData()
111 {
112 "keyword">foreach (var npc in allNPCs)
113 {
114 "keyword">if (npc == null) continue;
115
116 var data = "keyword">new NPCData
117 {
118 name = npc.npcName,
119 position = npc.transform.position,
120 relationshipLevel = npc.relationshipLevel,
121 hasGivenQuest = npc.hasGivenQuest,
122 questStatus = npc.questStatus,
123 emotionalState = npc.emotionalState,
124 memories = npc.memories.ToArray()
125 };
126
127 npcSaveData[npc.npcName] = data;
128 }
129
130 "comment">// Save to persistent storage
131 "keyword">string json = JsonUtility.ToJson("keyword">new NPCDataCollection { npcs = npcSaveData.Values.ToArray() });
132 PlayerPrefs.SetString("NPCData", json);
133 }
134
135 "keyword">void LoadNPCData()
136 {
137 "keyword">string json = PlayerPrefs.GetString("NPCData", "");
138 "keyword">if ("keyword">string.IsNullOrEmpty(json)) "keyword">return;
139
140 try
141 {
142 var collection = JsonUtility.FromJson<NPCDataCollection>(json);
143 npcSaveData.Clear();
144
145 "keyword">foreach (var data in collection.npcs)
146 {
147 npcSaveData[data.name] = data;
148 }
149
150 Debug.Log($"Loaded data ">for {npcSaveData.Count} NPCs");
151 }
152 catch (System.Exception e)
153 {
154 Debug.LogError($"Failed to load NPC data: {e.Message}");
155 }
156 }
157
158 "keyword">void ApplyNPCData(NPC npc, NPCData data)
159 {
160 npc.relationshipLevel = data.relationshipLevel;
161 npc.hasGivenQuest = data.hasGivenQuest;
162 npc.questStatus = data.questStatus;
163 npc.emotionalState = data.emotionalState;
164 npc.memories = "keyword">new List<NPCMemory>(data.memories);
165
166 "comment">// Restore position "keyword">if needed
167 "keyword">if (data.position != Vector3.zero)
168 {
169 npc.transform.position = data.position;
170 }
171 }
172
173 "keyword">public NPC FindNPCByName("keyword">string name)
174 {
175 "keyword">return allNPCs.Find(npc => npc.npcName == name);
176 }
177
178 "keyword">public NPC[] GetNPCsByType(NPCType type)
179 {
180 "keyword">return allNPCs.Where(npc => npc.npcType == type).ToArray();
181 }
182
183 "keyword">public NPC[] GetNPCsByFaction("keyword">string faction)
184 {
185 "keyword">return allNPCs.Where(npc => npc.faction == faction).ToArray();
186 }
187}
188
189[System.Serializable]
190"keyword">public "keyword">class NPCData
191{
192 "keyword">public "keyword">string name;
193 "keyword">public Vector3 position;
194 "keyword">public "keyword">float relationshipLevel;
195 "keyword">public "keyword">bool hasGivenQuest;
196 "keyword">public QuestStatus questStatus;
197 "keyword">public EmotionalState emotionalState;
198 "keyword">public NPCMemory[] memories;
199}
200
201[System.Serializable]
202"keyword">public "keyword">class NPCDataCollection
203{
204 "keyword">public NPCData[] npcs;
205}
NPC Creation Best Practices
🎯 Purpose & Role
- Define clear purpose for each NPC
- Give them unique personality traits
- Consider their role in the world
- Make them memorable and distinct
💬 Dialogue Design
- Write contextual, varied dialogue
- Reflect personality in speech patterns
- Respond to player actions and reputation
- Include ambient and reaction dialogue
🎮 Interaction Quality
- Provide multiple interaction types
- Make interactions feel meaningful
- Offer player choice and consequences
- Implement feedback and reactions
⚡ Performance
- Optimize NPC update frequency
- Use object pooling for temporary NPCs
- Implement LOD for distant NPCs
- Cache frequently accessed data