当前位置:网站首页>Unity_Demo | 中世纪风3D-RPG游戏
Unity_Demo | 中世纪风3D-RPG游戏
2022-07-21 07:58:00 【米莱虾】
* 由于大三期末N个大课设轮番轰炸,停下了手里的好多事。
故时隔一月余久,我又去继续催化RPG小游戏Demo了。
* 此次短暂优化之后,基本的战斗系统、对话系统和背包系统已具雏形,
画面渲染也较为惹眼舒适了。
* 不知不觉,实习已近一月,在mentor的指导和同事的帮助下,成功接手并完成了一些开发业务单,明天开始为期两周左右的GameJam了,暂且搁置这一Demo探索。
* 等新鲜的-科技风-元素塔防出炉之后,再来和大家分享可以试玩的作品。
* 先有蛋还是先有鸡?反正先发B站才方便插视频URL hh~
️部分场景展示:
️项目的架构大致如下:
在此次的 Demo 制作中,借用了 Unity Asset Store 的一些免费资源,效果还是不错的
比如下面这个 Free SkyBox,可以呈现一个基础的3D天空场景
其实还是比较 beautiful 的对不对? 这样的对目前来说其实也够用了
将 Materials 中的 Skybox 拖进 Hierarchy 中即可产生效果,主要是Unity的版本要 > 2019.4.0
在初步制作的时候,我们需要在基础之上对一些 Bug 进行纠错 (主要是效果展现上的差距和程序上的不完善),最终不断丰富我们的表现效果。
要考虑的东西有很多:
️比如如何设计角色移动和攻击方式 (在 Unity 客户端中,可以像我一样利用鼠标响应,点击即立刻前往,点击并拖拽光标能朝着光标拖拽的方向即时丝滑移动。当停止移动并在攻击范围之内,即可点击敌人进行攻击。移动Move() 与 攻击Combat() 的细节逻辑处理也是一个重要的东西,是利用了混合树结合代码逻辑解决的);
️比如死亡的对象要进行销毁,使它不再具有物理意义,也要注意不要让死亡的NPC跟随我们的角色移动,避免造成一种混乱的现象。
️比如一个有地势差异的比较大的场景混合各种小场景,如何比较好的处理角色能否移动,这个时候我们就要利用 Bake烘焙辅助处理,通过控制 Navigation 中 Bake 的属性值来准确控制表现效果,如下图:
NavMesh 与 Bake 具体可以参考下面两篇文章:
Unity | 深入了解NavMeshAgent_米莱虾的博客-CSDN博客_navmeshagent 详解
Unity | Navmesh自动寻路运行报错分析与解决方案_米莱虾的博客-CSDN博客
️比如我们如何将视角绑定在角色身上或者别的想要被绑定的 target 上,这就要用到跟随相机,在 Camera 下挂载 Follow Camera,将 Follow Camera 调整到距离 target 合适的位置上并且与我们的目标绑定(挂载),从而达到一个视角跟随主人公移动的效果,但其实没几行代码...
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace RPG.Core
{
public class FollowCamera : MonoBehaviour
{
[SerializeField] Transform target;
void LateUpdate()
{
transform.position = target.position;
}
}
}
其他一些具体的细节以及优化有机会再和大家分享,下面呈现部分重要的代码
️Fighter.cs (主要是我们角色战斗逻辑的一些处理)
using UnityEngine;
using RPG.Movement;
using RPG.Core;
using GameDevTV.Saving;
using RPG.Attributes;
using RPG.Stats;
using System.Collections.Generic;
using GameDevTV.Utils;
using System;
using GameDevTV.Inventories;
namespace RPG.Combat
{
public class Fighter : MonoBehaviour, IAction
{
[SerializeField] float timeBetweenAttacks = 1f;
[SerializeField] Transform rightHandTransform = null;
[SerializeField] Transform leftHandTransform = null;
[SerializeField] WeaponConfig defaultWeapon = null;
[SerializeField] float autoAttackRange = 4f;
Health target;
Equipment equipment;
float timeSinceLastAttack = Mathf.Infinity;
WeaponConfig currentWeaponConfig;
LazyValue<Weapon> currentWeapon;
private void Awake() {
currentWeaponConfig = defaultWeapon;
currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon);
equipment = GetComponent<Equipment>();
if (equipment)
{
equipment.equipmentUpdated += UpdateWeapon;
}
}
private Weapon SetupDefaultWeapon()
{
return AttachWeapon(defaultWeapon);
}
private void Start()
{
currentWeapon.ForceInit();
}
private void Update()
{
timeSinceLastAttack += Time.deltaTime;
if (target == null) return;
if (target.IsDead())
{
target = FindNewTargetInRange();
if (target == null) return;
}
if (!GetIsInRange(target.transform))
{
GetComponent<Mover>().MoveTo(target.transform.position, 1f);
}
else
{
GetComponent<Mover>().Cancel();
AttackBehaviour();
}
}
public void EquipWeapon(WeaponConfig weapon)
{
currentWeaponConfig = weapon;
currentWeapon.value = AttachWeapon(weapon);
}
private void UpdateWeapon()
{
var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
if (weapon == null)
{
EquipWeapon(defaultWeapon);
}
else
{
EquipWeapon(weapon);
}
}
private Weapon AttachWeapon(WeaponConfig weapon)
{
Animator animator = GetComponent<Animator>();
return weapon.Spawn(rightHandTransform, leftHandTransform, animator);
}
public Health GetTarget()
{
return target;
}
public Transform GetHandTransform(bool isRightHand)
{
if (isRightHand)
{
return rightHandTransform;
}
else
{
return leftHandTransform;
}
}
private void AttackBehaviour()
{
transform.LookAt(target.transform);
if (timeSinceLastAttack > timeBetweenAttacks)
{
// This will trigger the Hit() event.
TriggerAttack();
timeSinceLastAttack = 0;
}
}
private Health FindNewTargetInRange()
{
Health best = null;
float bestDistance = Mathf.Infinity;
foreach (var candidate in FindAllTargetsInRange())
{
float candidateDistance = Vector3.Distance(
transform.position, candidate.transform.position);
if (candidateDistance < bestDistance)
{
best = candidate;
bestDistance = candidateDistance;
}
}
return best;
}
private IEnumerable<Health> FindAllTargetsInRange()
{
RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position,
autoAttackRange, Vector3.up);
foreach (var hit in raycastHits)
{
Health health = hit.transform.GetComponent<Health>();
if (health == null) continue;
if (health.IsDead()) continue;
if (health.gameObject == gameObject) continue;
yield return health;
}
}
private void TriggerAttack()
{
GetComponent<Animator>().ResetTrigger("stopAttack");
GetComponent<Animator>().SetTrigger("attack");
}
// Animation Event
void Hit()
{
if(target == null) { return; }
float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
BaseStats targetBaseStats = target.GetComponent<BaseStats>();
if (targetBaseStats != null)
{
float defence = targetBaseStats.GetStat(Stat.Defence);
damage /= 1 + defence / damage;
}
if (currentWeapon.value != null)
{
currentWeapon.value.OnHit();
}
if (currentWeaponConfig.HasProjectile())
{
currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
}
else
{
target.TakeDamage(gameObject, damage);
}
}
void Shoot()
{
Hit();
}
private bool GetIsInRange(Transform targetTransform)
{
return Vector3.Distance(transform.position, targetTransform.position) < currentWeaponConfig.GetRange();
}
public bool CanAttack(GameObject combatTarget)
{
if (combatTarget == null) { return false; }
if (!GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) &&
!GetIsInRange(combatTarget.transform))
{
return false;
}
Health targetToTest = combatTarget.GetComponent<Health>();
return targetToTest != null && !targetToTest.IsDead();
}
public void Attack(GameObject combatTarget)
{
GetComponent<ActionScheduler>().StartAction(this);
target = combatTarget.GetComponent<Health>();
}
public void Cancel()
{
StopAttack();
target = null;
GetComponent<Mover>().Cancel();
}
private void StopAttack()
{
GetComponent<Animator>().ResetTrigger("attack");
GetComponent<Animator>().SetTrigger("stopAttack");
}
}
}
️PlayerController.cs (主要是我们角色控制逻辑的一些处理,包括角色的自动寻路、和UI的交互、技能、和组件的交互、移动的交互、射线投射...)
using RPG.Combat;
using RPG.Movement;
using UnityEngine;
using RPG.Attributes;
using System;
using UnityEngine.EventSystems;
using UnityEngine.AI;
using GameDevTV.Inventories;
namespace RPG.Control
{
public class PlayerController : MonoBehaviour
{
Health health;
ActionStore actionStore;
[System.Serializable]
struct CursorMapping
{
public CursorType type;
public Texture2D texture;
public Vector2 hotspot;
}
[SerializeField] CursorMapping[] cursorMappings = null;
[SerializeField] float maxNavMeshProjectionDistance = 1f;
[SerializeField] float raycastRadius = 1f;
[SerializeField] int numberOfAbilities = 6;
bool isDraggingUI = false;
private void Awake() {
health = GetComponent<Health>();
actionStore = GetComponent<ActionStore>();
}
private void Update()
{
if (InteractWithUI()) return;
if (health.IsDead())
{
SetCursor(CursorType.None);
return;
}
UseAbilities();
if (InteractWithComponent()) return;
if (InteractWithMovement()) return;
SetCursor(CursorType.None);
}
private bool InteractWithUI()
{
if (Input.GetMouseButtonUp(0))
{
isDraggingUI = false;
}
if (EventSystem.current.IsPointerOverGameObject())
{
if (Input.GetMouseButtonDown(0))
{
isDraggingUI = true;
}
SetCursor(CursorType.UI);
return true;
}
if (isDraggingUI)
{
return true;
}
return false;
}
private void UseAbilities()
{
for (int i = 0; i < numberOfAbilities; i++)
{
if (Input.GetKeyDown(KeyCode.Alpha1 + i))
{
actionStore.Use(i, gameObject);
}
}
}
private bool InteractWithComponent()
{
RaycastHit[] hits = RaycastAllSorted();
foreach (RaycastHit hit in hits)
{
IRaycastable[] raycastables = hit.transform.GetComponents<IRaycastable>();
foreach (IRaycastable raycastable in raycastables)
{
if (raycastable.HandleRaycast(this))
{
SetCursor(raycastable.GetCursorType());
return true;
}
}
}
return false;
}
RaycastHit[] RaycastAllSorted()
{
RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius);
float[] distances = new float[hits.Length];
for (int i = 0; i < hits.Length; i++)
{
distances[i] = hits[i].distance;
}
Array.Sort(distances, hits);
return hits;
}
private bool InteractWithMovement()
{
Vector3 target;
bool hasHit = RaycastNavMesh(out target);
if (hasHit)
{
if (!GetComponent<Mover>().CanMoveTo(target)) return false;
if (Input.GetMouseButton(0))
{
GetComponent<Mover>().StartMoveAction(target, 1f);
}
SetCursor(CursorType.Movement);
return true;
}
return false;
}
private bool RaycastNavMesh(out Vector3 target)
{
target = new Vector3();
RaycastHit hit;
bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
if (!hasHit) return false;
NavMeshHit navMeshHit;
bool hasCastToNavMesh = NavMesh.SamplePosition(
hit.point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas);
if (!hasCastToNavMesh) return false;
target = navMeshHit.position;
return true;
}
private void SetCursor(CursorType type)
{
CursorMapping mapping = GetCursorMapping(type);
Cursor.SetCursor(mapping.texture, mapping.hotspot, CursorMode.Auto);
}
private CursorMapping GetCursorMapping(CursorType type)
{
foreach (CursorMapping mapping in cursorMappings)
{
if (mapping.type == type)
{
return mapping;
}
}
return cursorMappings[0];
}
public static Ray GetMouseRay()
{
return Camera.main.ScreenPointToRay(Input.mousePosition);
}
}
}
边栏推荐
- What is the difference between int *const p= & I and int const *p= & I
- LeetCode 10. 正则表达式匹配
- Mysql 5.7 配置复制
- 零基础转行软件测试学习要不要报培训班学习,还是自学好?
- Word2vec simple summary
- 音频自动增益控制 AGC 解决的问题及原理解析
- English语法_指示代词 this / these / that / those
- When Lenovo Xiaoxin Air13 Pro reinstalls win10, the storage device driver cannot be found
- 新知识经济时代,谁在生产知识?
- 【集训DAY8】Tent【数学】【DP】
猜你喜欢
好用的办公网优化工具OneDNS
FreeRTOS -- a method to detect the usage of task stack
3、Nacos 配置中心源码解析之 项目结构
CRM 概念:了解Leads、Prospect、MQL 和 SQL 的概念
StarkNet如何改变L2格局?
Database connection pool
【愚公系列】2022年7月 Go教学课程 014-运算符之算术运算符
easyexcel简单使用
[Yugong series] go teaching course in July 2022 014 arithmetic operators of operators
Easyexcel is easy to use
随机推荐
LeetCode 10. Regular Expression Matching
【集训DAY8】Series【矩阵乘法】
2022年化工自动化控制仪表考试试题模拟考试平台操作
How to understand pointers?
Mysql03 (Association query)
【着色器实现Television信号三原色闪烁效果_Shader效果第五篇】
[terminal _1]-xshell 5 the hottest terminal software!
工控软件——驱动框架
05-1. Default member function: copy constructor, assignment operator overload
动态规划背包问题——01背包
c语言进阶篇:数据的存储(深度剖析-整型)
JVM 内存布局详解,图文并茂,写得太好了!
【sciter】:窗口通信
七甲川染料CY7标记肽核酸PNA合成方法CY7-PNA
【集训DAY6】Dream【优先队列】【贪心】
Mysql06(序列)
Pyqt5 packaging error, missing files, such as importerror: opencv loader: missing configuration file: ['config.py'] Check
[Yugong series] go teaching course in July 2022 014 arithmetic operators of operators
Visualization: you must know these ten data visualization tool software platforms
Learn the necessary tools of automation selenium think about automated testing in the pit again