在 Unity 中构建 3D 游戏:从概念到完成
现代视频游戏的发展代表了多个学科的独特融合:计算机科学算法、玩家参与的心理学原理和交互设计理论。通过开发在 Unity 中创建的 3D roguelike 游戏“寄生虫”,本文研究了理论计算机科学概念如何在实际游戏开发中实现,同时考虑了它们对玩家心理学和游戏设计原则的影响。
理论框架
计算机科学基础
“寄生虫”中人工智能的实现建立在几个基本的计算机科学概念之上:
- 图论和寻路
- A* 算法在空间导航中的应用
- 3D 空间中的图遍历优化
- 实现启发式函数以提高性能
- 有限状态机 (FSM)
- 行为建模的状态转换逻辑
- 实践中的确定性自动机
- 事件驱动型架构实施
心理学框架
AI 系统设计结合了关键的心理学原则:
- 玩家参与度理论
- 挑战技能平衡维护
- 流状态优化
- 渐进式难度缩放
- 行为心理学
- 敌人行为中的刺激-反应模式
- 强化学习原则
- 通过一致的 AI 响应进行球员调节
游戏概述
《寄生虫》是一款 3D Roguelike 游戏,玩家可以在其中穿越程序生成的环境,同时面对 AI 驱动的敌人。该游戏将战略战斗与动态敌人行为相结合,创造了引人入胜的玩家体验。
技术实施
1.A* 寻路系统
我们的核心技术挑战之一是实现智能敌人移动。以下是我们的处理方法:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">PathFinder</span> <span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">MonoBehaviour</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">Grid3D</span> <span style="color:var(--syntax-text-color)">grid</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">List</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Node</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-text-color)">openSet</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">HashSet</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Node</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-text-color)">closedSet</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-text-color)">List</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Vector3</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-name-color)">FindPath</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">Vector3</span> <span style="color:var(--syntax-text-color)">startPos</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">Vector3</span> <span style="color:var(--syntax-text-color)">targetPos</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">Node</span> <span style="color:var(--syntax-text-color)">startNode</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">grid</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">NodeFromWorldPoint</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">startPos</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">Node</span> <span style="color:var(--syntax-text-color)">targetNode</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">grid</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">NodeFromWorldPoint</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">targetPos</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">openSet</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-text-color)">List</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Node</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-text-color)">startNode</span> <span style="color:var(--syntax-text-color)">};</span>
<span style="color:var(--syntax-text-color)">closedSet</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-text-color)">HashSet</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Node</span><span style="color:var(--syntax-text-color)">>();</span>
<span style="color:var(--syntax-declaration-color)">while</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">openSet</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Count</span> <span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-literal-color)">0</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">Node</span> <span style="color:var(--syntax-text-color)">currentNode</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-name-color)">GetLowestFCostNode</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-comment-color)">// A* implementation logic</span>
<span style="color:var(--syntax-comment-color)">// ...</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-name-color)">RetracePath</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">startNode</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">targetNode</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">Node</span> <span style="color:var(--syntax-name-color)">GetLowestFCostNode</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-comment-color)">// Implementation details</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
A* 实现允许敌人:
- 找到通往玩家的最佳路径
- 绕过障碍物
- 适应动态环境变化
2. 用于敌人 AI
的有限状态机我们实施了一个强大的 FSM 系统来管理敌人的行为:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">public</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">EnemyFSM</span> <span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">MonoBehaviour</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">enum</span> <span style="color:var(--syntax-text-color)">State</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">Idle</span><span style="color:var(--syntax-text-color)">,</span>
<span style="color:var(--syntax-text-color)">Patrol</span><span style="color:var(--syntax-text-color)">,</span>
<span style="color:var(--syntax-text-color)">Chase</span><span style="color:var(--syntax-text-color)">,</span>
<span style="color:var(--syntax-text-color)">Attack</span><span style="color:var(--syntax-text-color)">,</span>
<span style="color:var(--syntax-text-color)">Retreat</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">State</span> <span style="color:var(--syntax-text-color)">currentState</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">Dictionary</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">System</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Action</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-text-color)">stateActions</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">void</span> <span style="color:var(--syntax-name-color)">Start</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">stateActions</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-text-color)">Dictionary</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">System</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Action</span><span style="color:var(--syntax-text-color)">>()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Idle</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">IdleState</span> <span style="color:var(--syntax-text-color)">},</span>
<span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Patrol</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">PatrolState</span> <span style="color:var(--syntax-text-color)">},</span>
<span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Chase</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">ChaseState</span> <span style="color:var(--syntax-text-color)">},</span>
<span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Attack</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">AttackState</span> <span style="color:var(--syntax-text-color)">},</span>
<span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Retreat</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">RetreatState</span> <span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">};</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">void</span> <span style="color:var(--syntax-name-color)">Update</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">stateActions</span><span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-text-color)">currentState</span><span style="color:var(--syntax-text-color)">].</span><span style="color:var(--syntax-name-color)">Invoke</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-name-color)">CheckTransitions</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
3. 状态转换和决策每个
状态都实现特定的行为和转换逻辑:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">void</span> <span style="color:var(--syntax-name-color)">ChaseState</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-comment-color)">// Use A* pathfinding to chase player</span>
<span style="color:var(--syntax-text-color)">Vector3</span> <span style="color:var(--syntax-text-color)">pathToPlayer</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">pathFinder</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">FindPath</span><span style="color:var(--syntax-text-color)">(</span>
<span style="color:var(--syntax-text-color)">transform</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">position</span><span style="color:var(--syntax-text-color)">,</span>
<span style="color:var(--syntax-text-color)">playerPosition</span>
<span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-comment-color)">// Move along path</span>
<span style="color:var(--syntax-name-color)">MoveAlongPath</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">pathToPlayer</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-comment-color)">// Check attack range</span>
<span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">InAttackRange</span><span style="color:var(--syntax-text-color)">())</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-name-color)">TransitionToState</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">State</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">Attack</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
性能优化
为了保持流畅的游戏体验,我们实施了多种优化技术:
路径缓存
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-text-color)">Dictionary</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Vector3</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">List</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Vector3</span><span style="color:var(--syntax-text-color)">>></span> <span style="color:var(--syntax-text-color)">pathCache</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">float</span> <span style="color:var(--syntax-text-color)">pathUpdateInterval</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-literal-color)">0.5f</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">void</span> <span style="color:var(--syntax-name-color)">CachePath</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">Vector3</span> <span style="color:var(--syntax-text-color)">start</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">Vector3</span> <span style="color:var(--syntax-text-color)">end</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">List</span><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-text-color)">Vector3</span><span style="color:var(--syntax-text-color)">></span> <span style="color:var(--syntax-text-color)">path</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">string</span> <span style="color:var(--syntax-text-color)">key</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-name-color)">GetPathKey</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">start</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-text-color)">end</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">pathCache</span><span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-text-color)">key</span><span style="color:var(--syntax-text-color)">]</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">path</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
State Update 限制
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">float</span> <span style="color:var(--syntax-text-color)">lastStateUpdate</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">private</span> <span style="color:var(--syntax-declaration-color)">float</span> <span style="color:var(--syntax-text-color)">stateUpdateInterval</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-literal-color)">0.2f</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">void</span> <span style="color:var(--syntax-name-color)">Update</span><span style="color:var(--syntax-text-color)">()</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">if</span> <span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-text-color)">Time</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">time</span> <span style="color:var(--syntax-text-color)">-</span> <span style="color:var(--syntax-text-color)">lastStateUpdate</span> <span style="color:var(--syntax-text-color)">>=</span> <span style="color:var(--syntax-text-color)">stateUpdateInterval</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">stateActions</span><span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-text-color)">currentState</span><span style="color:var(--syntax-text-color)">].</span><span style="color:var(--syntax-name-color)">Invoke</span><span style="color:var(--syntax-text-color)">();</span>
<span style="color:var(--syntax-text-color)">lastStateUpdate</span> <span style="color:var(--syntax-text-color)">=</span> <span style="color:var(--syntax-text-color)">Time</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-text-color)">time</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
挑战和解决方案
挑战 1:路径重新计算开销
最初,我们的敌人每帧都会重新计算路径,这会导致性能问题。我们通过以下方式解决了这个问题:
- 实现路径缓存
- 添加更新间隔
- 使用路径预测进行细微调整
挑战 2:状态转换边缘案例我们
遇到了敌人会在状态之间快速切换的错误。溶液:
- 添加了状态转换冷却时间
- 在决策中实施滞后
- 创建详细的状态转换规则
结果和指标
我们的优化带来了显著的改进:
- CPU 使用率降低 40%
- 与 20+ 活跃敌人保持 60+ FPS
- 平滑的状态转换,最小的卡顿
经验 教训
- 规划您的架构:从长远来看,设计良好的 FSM 可以节省时间
- 及早分析:定期性能分析有助于识别瓶颈
- 智能优化:专注于影响玩家体验的优化
- 测试边缘案例:状态机需要对转换案例进行稳健测试
后续步骤我们正在考虑
的未来改进:
- 为更复杂的 AI 实现行为树
- 添加动态难度调整
-
为不同的敌人类型
扩展状态系统资源
对于那些有兴趣实现类似系统的人: -
Unity 的导航网格文档
-
A* 探路项目
-
Game Programming Patterns - 状态模式
结论
构建寄生虫是一项令人兴奋的挑战,它教会了我们有关游戏 AI 实现的宝贵经验。A* 寻路和 FSM 的结合创造了引人入胜的敌人行为,同时保持了良好的性能。
原文地址:https://blog.csdn.net/u013528853/article/details/143771887
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!