前言
该项目为大三课设,本人为开发程序员,另外两位同学负责文案、剧情以及美工,开发经验不足,下文为游戏设计书以及其他文档,仅供学习参考。
演示视频如下:
一、概述
1.1项目名称
King Arthur Loyalty and Betray
1.2项目简介
King Arthur Loyalty and Betray是由三人成行小组基于Unity游戏引擎自主开发的第三人称RPG游戏原型。
1.3本文结构
本文分为概述、游戏故事、游戏元素设定、玩法设计、游戏交互设计、游戏界面设计、游戏进程和关卡设计、程序设计、游戏系统测试九个部分。
1.4成员分工
组长:汪洛飞 项目管理 代码编写 系统测试 游戏美工 协助数值策划
组员:杨宜松 协助游戏美工 协助游戏文案 数值策划
组员:周科宇 角色设计 游戏文案 地图设计
1.5游戏类型和其他同类型游戏比较
拥有独特的战斗系统以及解密机制
1.6游戏分级
ESRB分级制度:T级,适合13岁及以上玩家
二、游戏故事
2.1游戏故事设定
中世纪,统治卡美洛王国的尤瑟王去世后,卡美洛王国外受罗马帝国入侵,内有叛军作乱,为抵御内忧外患,举行选王大比。
2.2游戏故事梗概
年幼的亚瑟自小便在艾克特爵士的教导下,与兄长艾克特爵士之子凯一起健康而出色地成长,并在安排好的意外中,不觉间拔出了选王之剑,成功一统大不列颠。之后,亚瑟王四处平叛战乱,收复最初的十二圆桌骑士,并踏上了寻找传说中的圣物“圣杯”的路途,在旅途中,他又得到某人的指引,见到了湖中的仙子……可最终,圆桌并不统一,王权之争开启,在决战中,亚瑟王身受致命伤,最后静静殒命……
2.3主要角色设计
2.3.1角色背景
亚瑟·潘德拉贡:尤瑟之子,自幼跟随艾克特爵士,正直勇敢,武艺高强,在一场事先安排好的意外中,拔出选王之剑,踏上了统一大不列颠的旅程。
2.3.2角色艺术设计
人物模型下载于mixamo网站
三、游戏元素设定
3.1游戏角色
艾克特 主线剧情关键人物
梅林 主线剧情关键人物
商人 支线剧情关键人物 提供交易功能
铁匠 支线剧情关键人物 提供合成功能
3.2游戏物品
(下表仅展现物品部分属性,描述文本有所省略,详细信息见StreamingAsserts下ItemInfo.json)
Quality:0,1,2,3,4,5,6分别对应普通、稀有,罕见,史诗,传奇,神器,不可掉落
Type:0,1,2,3分别对应消耗品,武器,防具,材料
Id | Name | Quality | Effect | BuyPrice | Img | Description | Type |
1 | 生命药剂 | 0 | 恢复50HP | 20 | ![]() | 回复生命的药剂 | 0 |
2 | 魔力药剂 | 0 | 恢复50MP | 20 | ![]() | 富含魔力的药剂 | 0 |
3 | 狂战药剂 | 1 | 失去10%当前生命,20s基础攻击加10% | 50 | ![]() | 激发战意的药水但会增加身体的负担 | 0 |
4 | 巫术合剂 | 2 | 60s内受到伤害增加20%造成伤害加50% | 100 | ![]() | 混入邪恶力量的药剂,增强技能效果,但会使精神混乱 | 0 |
5 | 强化药水 | 3 | 30s内暴击率增加20%,暴击伤害增加50% | 150 | ![]() | 蕴含强大力量的药剂 | 0 |
6 | 嗜血药 | 4 | 40s内普通攻击5%用于回复自己 | 200 | ![]() | 食人花花粉制作的药剂 | 0 |
7 | 铁剑 | 0 | 50 | ![]() | 一把还算趁手的武器 | 1 | |
8 | 灵剑 | 1 | 250 | ![]() | 蕴含魔力的剑 | 1 | |
9 | 雷霆之剑 | 2 | 装备时获得技能:“闪电风暴” | 400 | ![]() | 寄宿雷电之灵的剑,能够发动雷属性魔法 | 1 |
10 | 诅咒之刃 | 3 | 每秒损失当前生命值5%,但伤害10%转化为生命值 | 600 | ![]() | 被诅咒的魔器,但蕴含巨大力量 | 1 |
11 | 守护之剑 | 4 | 普通攻击额外造成敌人当前8%生命 | 1000 | ![]() | 为守护所爱之人而锻造的利剑 | 1 |
12 | 烈焰之剑 | 5 | 获得技能“火焰风暴 | 1500 | ![]() | 寄宿火焰之灵的剑,能够发动火系魔法 | 1 |
13 | 尘封之剑 | 6 | 50 | ![]() | 剑虽尘封尤可斩魔 | 1 | |
14 | 破晓之剑 | 6 | 每次攻击命中基础攻击加10%5s | 50 | ![]() | 枪出如龙剑出破晓 | 1 |
15 | 王者之剑 | 6 | 每次攻击5%直接击败对方 | 50 | ![]() | 永失吾爱,举目破败 | 1 |
16 | 皮甲 | 0 | ![]() | 聊胜于无的防具 | 2 | ||
17 | 振奋盔甲 | 1 | 装备时获得技能治疗之环 | ![]() | 附着治愈魔法的铠甲 | 2 | |
18 | 魔剑士铠甲 | 2 | ![]() | 能够增幅魔法的铠甲 | 2 | ||
19 | 诅咒之铠 | 3 | 最大生命减少50%攻击力增加30% | ![]() | 蕴含诅咒力量的魔剑,将生命转化为力量 | 2 | |
20 | 英灵铠 | 4 | 防御增加20% | 1500 | ![]() | 寄宿英雄执念的盔甲 | 2 |
21 | 火焰甲 | 5 | 装备时获得技能”召唤火圈” | 2000 | ![]() | 寄宿火焰之灵的铠甲,能够发动火系魔法 | 2 |
22 | 大骨 | 0 | 10 | ![]() | 恶灵骷髅的掉落物,骨粉能够激发战意 | 3 | |
23 | 暗影核心 | 1 | 20 | ![]() | 暗影眷属核心能够增幅魔力 | 3 | |
24 | 史莱姆凝胶 | 1 | 20 | ![]() | 消灭史莱姆掉落,可以储存魔力 | 3 | |
25 | 食人花花苞 | 2 | 30 | ![]() | 食人花花苞,花粉是重要原材料 | 3 | |
26 | 水晶矿 | 6 | 500 | ![]() | 珍贵的矿石材料 | 3 | |
27 | 铁矿 | 0 | 50 | ![]() | 常见的矿石材料 | 3 |
3.3游戏机关
火焰 跟随将其送回火炬后解锁宝箱
隐藏的宝箱 按下F交互后开启
镇守的宝箱 击杀看守的敌人后可解锁
石中剑 在特定任务阶段长按F可交互
告示牌,靠近后按下F可查看信息
与众不同的树 靠近后按下F解锁宝箱
喷泉 死亡重生点 靠近按下F回复全状态
四、玩法设计
4.1游戏机制
4.1.1玩家能力
按下F与可交互物品交互
使用普通攻击以及武器技能击杀敌人
在商人处购买装备以及材料
铁匠处合成装备以及药剂
与酒鬼进行游戏
打开背包装备装备强化自身
4.1.2游戏胜负判定
无严格胜负判定,血量清空后重生,游戏目的以推进主线剧情为主
4.1.3系统
与酒鬼进行游戏下注赢取奖励
与商人交互后花费金币购买材料以及装备,可筛选物品
击杀怪物后获得经验,人物属性随等级变化而变化
按下B打开背包,左键背包格装备,右键背包格消耗,左键装备格卸下
按下V键打开任务栏追踪当前接取任务,部分任务需完成前置任务后或满足限定 条件后可接取,可同时接取多个任务。
根据配方在铁匠处放入正确物品可进行合成
4.2游戏行为规则
玩家行为产生的如开启宝箱,完成解密,完成任务,购买物品,击杀怪物等行为都会对游戏世界产生影响
非玩家角色能够提供给玩家交易合成以及其他互动功能,帮助玩家强化自身能力
非玩家角色在游戏中彼此互动主要通过主角完成任务的方式进行
解密行为能够帮助玩家找到宝箱,通过开启宝箱玩家能够强化自身
五、游戏交互设计
5.1人机交互设计
使用鼠标以及键盘进行交互
5.2鼠标和键盘操作定义
(可按esc在设置界面调整键位)
鼠标滚轮调整视角距离
按住右键调整视角方向
V打开任务列表
B打开背包
F常规交互
WASD移动
J普通攻击
短按H格挡长按H招架
E武器技能
R防具技能
六、游戏界面设计
6.1游戏界面设计原则
简洁醒目
6.2游戏界面草图
(设计时未绘制草图,但有相应的UI元素要求)
- 游戏载入界面
需要进度条以及载入背景图片
- 游戏介绍界面
左侧为图片,右侧为介绍信息
- 游戏选择界面
游戏标题,背景图片,开始游戏,继续游戏,保存游戏,退出游戏按钮
- 游戏设置界面
需要有自定义键位 主音量调节,背景音量调节,关闭按钮
- 任务清单界面
左侧为任务标题,右侧为任务具体信息,右上角为关闭按钮
- 下注界面
需要有筹码,奖金,游戏规则显示,需要开始,继续,结束,重置,关闭按钮。
- 商店界面
左侧为商品栏,有选择框,右侧为商人图片以及商人对话
- 合成界面
左侧为合成材料框,右侧为产品框,中间为合成按钮,右上角为关闭按钮
- 背包界面
中间为背包格,左下为装备栏,右下为人物属性栏,右上角为关闭按钮
- 游戏主界面
左上角为小地图,人物状态栏以及技能cd情况,右侧为拾取框,左侧为长久提示框,
下方为暂时提示框
6.3各界面跳转关系
6.3游戏美工设计
(详情见资源文件夹,可能有部分资源遗失)
除主角亚瑟的人物模型以及所有的界面图音频,特效均来自https://www.aigei.com/
人物动作除少量人物自带动作均来源于https://www.mixamo.com/
游戏转场图来自艾尔登法环素材,游戏人物立绘由ai绘画生成
七、游戏进程和关卡设计
7.1游戏进程
由于工作量原因,游戏原型仅包含一个关卡,为亚瑟拔出剑的故事剧情关卡
7.2关卡设计
7.2.1关卡包含的故事片段:
主线任务:亚瑟与父亲前往竞技场,父亲遗落剑在旅馆,亚瑟回去取发现旅馆关闭,梅林与只交谈后亚瑟拔出石中剑,释放出邪恶力量,梅林告知事情缘由。
支线任务1:亚瑟拔出石中剑释放出的邪恶力量导致墓园作祟,影响了商人的秘密行动,商人雇佣亚瑟前去消灭墓园里面的怪物。
支线任务2:铁匠发现亚瑟手中的剑为尘封的宝剑,提出为其恢复力量,但前提是亚瑟凑够三块水晶矿。
7.2.2关卡的结构和组织:
完成主线任务后后续支线任务才会开启。
7.2.3关卡的美工设计
俯视图,图中绿色为宝箱,黄色为特殊NPC
7.2.4关卡中玩家面对的主要的挑战、障碍或难题:
遗失的宝箱1:普通品质
遗失的宝箱2:稀有品质
遗失的宝箱3:稀有品质
遗失的宝箱1:稀有品质
镇守的宝箱:普通品质
镇守的宝箱2:普通品质
火焰解密
普通Npc1,农民Npc2猎人
特殊Npc1商人Npc2铁匠Npc3酒鬼
7.2.5关卡的初始状态:
初始所有npc在设定位置,特殊npc不会移动,普通npc按设定轨迹移动,
刷怪点初始刷怪为0
初始人物位于地图左下角旅馆处,满状态,背包空,等级1
7.2.6关卡的结束状态:
关卡未设置胜利条件以及失败条件,人物死亡后在复活点复活
八、程序设计
8.1游戏脚本结构
8.1.1Data目录:
Path.cs,Tag.cs使用static存储游戏资源路径以及标签信息
8.1.2Utils目录:
Util.cs游戏工具脚本,提供以下功能
- 获取指定返回随机整数
- 从json文件中读取字符串
- 向json文件中写入类信息
- 加载指定路径sprite
- 根据物品品质设置文本颜色
- 将成员为item的列表按指定类型顺序排序(用于物品筛选)
- 将整型数据转化为itemType枚举类型(用于将button传入int转化)
- 伤害计算函数
- 治疗计算函数
- 根据等级设置人物基础属性
- 根据中心以及半径返回圆形区域内随机坐标
- 将枚举类型itemType转化为对应文本描述
- 根据当前人物经验返回等级以及溢出经验
- 设置人物升到下一级所需经验、
- 读取键盘输入(用于自定义键位输入)
8.1.3ItemSystem目录:
存放与物品相关的脚本
8.1.3.1Item:
Item.cs:定义物品基类
提供无参构造函数,全参构造函数以及copyItem(返回复制的item而非引用)
ItemManager.cs:管理物品信息
提供以下功能
- 读取json文件获取全体物品信息并保存
- 根据物品id返回指定item
- 根据物品品质返回list<Item>
- 根据物品品质随机返回一个item
ItemObject.cs:管理物品gameobject
提供物品旋转,以及掉落随机方向射出效果,存储item信息
8.1.3.2Bag
BagManager.cs:存储背包中物品以及装备信息
提供以下功能
- json文件读取/写入背包物品信息
- json文件读取/写入装备信息
- json文件读取/写入金币信息
- 返回背包中物品
- 向背包中添加指定数目的指定物体
- 从背包中移除指定数目的指定物体
- 添加/减少金币
BagSlotsControlle.csr:根据背包信息实时管理背包UI,保存背包状态use或者forge
BagSlot.cs:管理单个背包格,储存item提供信息,根据背包状态左键响应不同
EquipmentSlot.cs:管理单个装备,储存item,左键卸下
8.1.3.3Shop:
ShopManager.cs
ShopSlotsController.cs
ShopSlot.cs
脚本功能与Bag目录相对应不再详细介绍
8.1.3.4Forge:
Formula.cs存储配方类,提供全参构造函数
ForgeManager.cs通过读取json文件存储配方信息,提供功能查询是否有匹配配方并返回产物,无则返回null
ForgeControlle.cs管理合成栏界面UI
OriginMaterialSlot.cs控制原材料格UI,点击后背包为select模式,从背包中获取物品信息
ProductSlot.ce控制产物格UI
8.1.3.5Pick:
ItemDect.cs存储碰撞器范围类所有物品信息
PickSlotsController.cs管理拾取界面UI,将ItemDect中信息传递给PickSlot
PickSlot管理拾取格UI,存储物品信息
8.1.3.6Effect:
Equip.cs提供装备效果,负责更新背包中装备栏item,但不直接参与装备属性加成
Consume.cs提供消耗效果,根据消耗物品ID直接管理药剂消耗效果
8.1.4SkillSystem目录
CureCircle.cs,FireCircle.cs,FireStorm.cs,LightingExplode.cs分别对应游戏中四种技能效果,
SkillController.cs负责管理技能
提供以下功能
- 根据装备ID释放对应技能,如果无技能返回false
- 根据装备ID返回对应技能图片路径,如果无技能返回透明图片路径
8.1.5DialogSystem目录
Dialog.cs提供对话类信息
DialogManager.cs根据路径解析json文件,存储List<Dialog>
DialogController.cs对话UI控制,根据Manager提供的List逐个显示
8.1.6TaskSystem目录
Task.cs提供任务类信息,关键属性:任务阶段数,任务对话json存储目录,后续解锁任务id号,其中阶段为0表示任务已经解锁但未接取
TaskManager.cs读取json文件存储所有任务信息,提供返回指定ID任务函数
TaskController.cs维护List<Task>,list中为所有开放的任务(每当有任务解锁时,加入list,完成则移出list)提供以下功能:
- 添加task到list
- 任务结算函数(移出list,获取奖励,将该任务后续任务加入list)
- 任务进入下一阶段
- 显示当前任务当前阶段对话
- 保存/读取list到json
TaskBar.cs管理任务栏ui,维护已经接取任务list(读取manager中list中stage>0的task),显示selectedTask详细信息。
TaskSlot.cs管理任务选择格ui,被选中后更改TaskBar中selectedTask信息。
TaskCarrier内定义Target{id,stage},储存信息Target[],(该数组在引擎中手动写入)主要功能当Manager中有能匹配Target[]的task时显示对话并让任务进入下一阶段,该脚本绑在预制体TaskCarrier上,创建任务时将预制体绑在对应npc上并赋值即可。
8.1.7TipSystem目录
Tip.cs定义提示UI,内部update中线性插值透明度实现渐变消失,定义TipType管理消失速度
Tipcontroller.cs管理Tip,维护Tip队列,当队列不为空时出Tip,等到Tip destroy后继续出
(缺陷:当有lastingTip类型的Tip时必须手动关掉Tip,否则队列不会Dequeue,暂未想到根除方法,现用解决方案为Tip为lastingTip时使用更加醒目的UI强迫玩家关掉)
TipManage.cs控制lastingTip只显示一次,lastingTip类型提示均为游戏引导型提示,应当只显示一次,使用全局变量来判断是否已经显示该Tip。(理论上应当将这些bool变量存到json文件中保存,否则继续游戏时仍会显示,但出于时间考虑以及担心新bug出现未实施)
8.1.8BuffSystem目录
BuffController.cs内部定义类Buff(属性为临时攻击等加成属性以及持续时间和开始时间
)BuffController维护Buff列表,当timer-startTimr超过lastingTime时移出队列,提供加入Buff函数,并将Buff列表中全体buff属性相加。
8.1.9GamblingSystem目录
GamblingSlot.cs控制骰子格ui,根据传入number显示不同的img。
GamblingController.cs内部函数如下:
- 增添筹码,并限制数目
- 设置下注属性
- 开始游戏,判断是否满足游戏条件并调用一次继续游戏,start为true
- 继续游戏,当index小于slot数且canContinue为true时随机一个骰子数并赋值给slot
- 结束游戏,将内部属性恢复,并调用BagManager添加金币
- update中进行匹配判定,决定canContinue的值,以及当前倍率
8.1.10InterAct目录
该目录主要管理游戏中可互动物品的交互,基本都为触发器调用其他类接口,此处不在赘述。
8.1.11Controller目录
Camera360.cs 控制视角的变化,复制于csdn博客稍作修改,(源代码使用transfrom移动,导致卡墙问题,修改为使用character.move移动)
GameController.cs该文件绑定在GameController这个始终为active的空物体上,(涉及controller,manager的脚本基本都绑在此物体上,否则会出现instance为空的情况导致出错),
发挥了utils的部分作用(utils未继承MonoBehavior),提供控制各种界面开关,以及player的state,pos人物状态ui控制,受击震动等作用。
IntroPanelController.cs控制介绍界面(点击显示下一张图片)
MinMapCameraFollow.cs小地图控制脚本,将minmapCamera拍摄的图片显示在界面上,并让箭头方向跟随人物旋转。
MusicController.cs,控制音乐,特定条件下播放特定音乐(原本预计进入战斗时播放音乐,但用触发器进行控制出现击杀怪物后音乐不停止或者战斗不播放音乐的bug,进行修改代码后曾出现过无bug出现的情况,但增加延时切换音乐后又会出现bug,最后忘记无bug版本的代码,由于时间关系放弃计划)
SceneController.cs,管理场景切换,使用异步加载场景添加进度条效果,以及传送时显示加载页面(此时进度条为假的进度条)防止因为camerafollow导致游戏体验较差的效果。
(原计划除主场景之外搭建副本场景,但单纯使用DonnotDestoryOnLoad会导致切换副本再回到主场景会导致有两个同名的gameobject,使用线性关卡模式倒不需要考虑这个问题,但搭建一个同主场景一样大的地图耗时太久,后续制作出的存档系统单纯的传数据不使用destoryonload或许可以解决此问题,但由于时间关系放弃计划)
SettingManager.cs控制键位信息,并提供将键位信息存储json文件以及从json文件中加载键位的功能。维护一个InputInfo类,里面存储KeyCode,当其他类需要使用键盘输入时调用InputInfo即可(后续将此功能封装在Utils.GetKeyDown函数中)
8.1.12Character目录
State.cs存储character的全部属性值(定义为Value类),以及状态isAttacked,canMove
和受到伤害累计harm提供loadValue从json文件中获取value便于怪物的批量管理
8.1.12.1Enemy
HPSlider.cs控制怪物血条
SpawnEnemyPoint.cs刷怪点,绑定在预制体SpawnEnemyPoint上,通过在引擎进行预设即可实现刷怪数量频率以及刷怪的类型
BlackEckert.cs控制黑化艾克特行为(在观看其他小组的boss战后制作的boss,原本剧情是eckert回来后黑化和亚瑟战斗,预设格挡,攻击,追击,周旋等行为,但写好部分功能后发现战斗体验较差,考虑时间关系放弃,如有兴趣,可将Prefabs中BlackEckert拖入场景)
Enemy.cs怪物的基类(虽然只实现了恶灵骷髅一种类型怪,导致只有一个子类)
定义枚举类型Behaviour控制怪物行为,值越大行为优先级越高,SetBehaviour,设置行为,但只有形参高于当前行为优先级才能被设置否则不变。
动画控制函数:AttackStart,Hit,Attackend,分别对应攻击开始,攻击命中,攻击结束事件,由动画中关键帧调用,由此实现格挡效果
基本行为函数:向目标前进,绕圈,警戒判断,攻击是否命中判断,死亡,死亡掉落。
动画函数控制,根据当前行为判断播放什么动画
行为函数控制,根据当前行为判断调用什么函数
Skeleton.cs恶灵骷髅类,在start中从json文件中加载value,update中首先重置Behaviour,防止因为上一帧执行高优先级行为后而无法还原。然后进行相应的行为判断设定对应的behaviour,最后调用动画函数控制以及行为函数控制。
8.1.12.2NPC
NPC.cs控制普通npc脚本,主要功能为移动,以及玩家接近时朝向玩家(原本应该有简单对话,但需要编写json文件取消)
Trader.cs,Smith.cs,Drinker.cs分别对应游戏三个特殊npc,脚本大同小异,当触发器触发时调用gamecontroller中函数使对应界面打开。
Eckert.cs控制任务npcEckert,主要行为为移动指定位置以及控制任务进程,前者同解密中火焰移动相同,后者直接调用taskSystem中类函数。
8.1.12.3Player
Player.cs start中根据是否为新游戏加载上次存档人物信息。以下为update各个部分介绍
玩家数值控制为首先调用utils.setBaseValuebyLevel根据state.value设定玩家的基础属性,再加上玩家的武器装备的属性再加上buff提供的临时属性。
连招攻击:当玩家为idle或者run时且攻击未冷却时可以发动第一段攻击,在攻击动画中有特定时间段combo为1,combo为1时即可发动二段三段攻击,并更新lastAttackTime。
翻滚:简单调用动画,此处动画不是inplace
格挡:跑翻滚静止时按下H时同时触发block,以及setbool blockidle true,前者为短暂的动画,在block结束后才播放blockIdle动画,松开H,setbool blockIdle fasle,实现短按弹反长按招架效果
当在block动画时state.isAttacked为true时,isAttacked为false,重置伤害并调用弹反攻击
当为blockIdle动画时 state.isAttacked为true,设置为false,重置伤害招架进入冷却
受伤判定为
isAttacked为true时置为false并受到state.harm的伤害。
NormalAttack.cs,普通攻击生效时使碰撞器内部受到伤害,不调用时不生效
BlockAttack.cs,弹反攻击,类似NormalAttack,但附带击退效果。
8.2主要脚本和算法
(Bag,Forge,Task)系统均使用MVC设计模式基本相似,这里使用Bag举例
BagManager为Model,储存背包数据
BagController为Controller,负责联系Model与View
BagSlot为View,管理背包UI
BagManager.cs
using System.Collections.Generic;
using UnityEngine;
#region 可序列化数据类
/// <summary>
/// 存储背包中物品信息
/// </summary>
[System.Serializable]
public class Bag
{
public List<Item> items;
}
/// <summary>
/// 储存背包中装备信息
/// </summary>
[System.Serializable]
public class Equipment
{
public Item weapon;
public Item armor;
public Item consumble;
public Equipment(Item weapon, Item armor, Item consumble)
{
this.weapon = weapon;
this.armor = armor;
this.consumble = consumble;
}
}
/// <summary>
/// 储存背包中金币信息
/// </summary>
[System.Serializable]
public class Money
{
public int amount;
public Money(int amount)
{
this.amount = amount;
}
}
#endregion
/// <summary>
/// 在数据层面更改背包物品信息
/// </summary>
public class BagManager :MonoBehaviour
{
public Bag bag;
public static BagManager instance;
public EquipmentSlot weaponSlot;
public EquipmentSlot armorSlot;
public EquipmentSlot consumbleSlot;
public int money;
private void Start()
{
instance = this;
if (SceneController.instance != null && !SceneController.instance.isNewGame)
{
LoadBagFromJson();
LoadEquipmentFromJson();
LoadMoneyFromJson();
}
}
private void Update()
{
Task task = TaskController.instance.FindTaskInOpenTaskById(3);
if(task!=null&&task.PresentStage==1)
{
Item item = FindItemInBag(ItemManager.GetItemById(26));
if(item!=null&&item.Amount>=2)
{
TaskController.instance.GoNextStage(task);
}
}
}
#region 保存信息
/// <summary>
/// 将背包信息保存为json文件
/// </summary>
public void SaveBagToJson()
{
Utils.WriteJsonData(bag, Path.BagPath);
}
public void SaveEquipmentToJson()
{
Equipment equipment = new Equipment(weaponSlot.item, armorSlot.item, consumbleSlot.item);
Utils.WriteJsonData(equipment, Path.EquipmentPath);
}
public void SaveMoneyToJson()
{
Money _money = new Money(money);
Utils.WriteJsonData(_money, Path.MoneyPath);
}
#endregion
#region 载入信息
/// <summary>
/// 从json文件中加载背包
/// </summary>
public void LoadBagFromJson()
{
string jsonData = Utils.GetJsonData(Path.BagPath);
bag = JsonUtility.FromJson<Bag>(jsonData);
}
public void LoadEquipmentFromJson()
{
string jsonData = Utils.GetJsonData(Path.EquipmentPath);
Equipment equipment= JsonUtility.FromJson<Equipment>(jsonData);
weaponSlot.item = equipment.weapon;
armorSlot.item = equipment.armor;
consumbleSlot.item = equipment.consumble;
}
public void LoadMoneyFromJson()
{
string jsonData = Utils.GetJsonData(Path.MoneyPath);
Money _money = JsonUtility.FromJson<Money>(jsonData);
money = _money.amount;
}
#endregion
#region 修改背包属性
/// <summary>
/// 向背包中添加指定数量物品
/// </summary>
/// <param name="item">物品</param>
/// <param name="amount">数量</param>
public void AddItemToBag(Item item,int amount,bool showTip)
{
if(showTip)
{
string info = "获得" + item.Name + " X " + amount;
TipController.instance.AddTip(info, Tip.TipType.Quick);
}
bool exist = false;
foreach(Item _item in bag.items)
{
if (_item.Id == item.Id)
{
_item.Amount += amount;
exist = true;
}
}
if (!exist)
{
Item _item = Item.CopyItem(item);
_item.Amount = amount;
bag.items.Add(_item);
}
}
/// <summary>
/// 从背包中移除物品
/// </summary>
/// <param name="item">物品</param>
/// <param name="amount">数目</param>
public void RemoveItemFromBag(Item item,int amount)
{
bool exist = false;
for (int i = bag.items.Count - 1; i >= 0; i--)
{
if (bag.items[i].Id==item.Id)
{
exist = true;
bag.items[i].Amount -= amount;
if (bag.items[i].Amount <= 0)
bag.items.RemoveAt(i);
}
}
if (!exist)
Debug.Log("no such item in bag");
}
public void AddMoney(int amount)
{
money += amount;
TipController.instance.AddTip("获得金币X" + amount, Tip.TipType.Quick);
}
public bool RemoveMoney(int amount)
{
if (money >= amount)
{
money -= amount;
return true;
}
else
return false;
}
#endregion
public Item FindItemInBag(Item item)
{
for(int i=0;i<bag.items.Count;i++)
{
if (item.Id == bag.items[i].Id)
return Item.CopyItem(bag.items[i]);
}
return null;
}
}
BagSlot.cs
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class BagSlot : MonoBehaviour,IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
public Image img;
public Item item;
public GameObject itemInfo;
public Text itemName;
public Text amount;
public Text description;
public Text effect;
public Text attribute;
public UnityEvent leftClick;
public UnityEvent rightClick;
// Start is called before the first frame update
void Start()
{
img = GetComponent<Image>();
leftClick.AddListener(new UnityAction(ButtonLeftClick));
rightClick.AddListener(new UnityAction(ButtonRightClick));
}
// Update is called once per frame
void Update()
{
if (item.Amount == 0)
{
itemInfo.SetActive(false);
img.sprite = Utils.LoadSprite("Textures/UI/ItemIcon/0000");
}
else
{
itemName.text = item.Name;
Utils.SetTextColorByQuality(itemName, item.Quality);
amount.text = item.Amount.ToString();
description.text = item.Description;
effect.text = item.Effect;
attribute.text = ((item.Attack > 0) ? "攻击力:" + item.Attack.ToString() + "\n" : "") + ((item.Defense > 0) ? "防御力:" + item.Defense.ToString() + "\n" : "") +
((item.CritChance > 0) ? "暴击率:" + item.CritChance.ToString() + "\n:" : "")+((item.CritRate>0)?"暴击伤害:"+item.CritRate.ToString()+"\n":"");
img.sprite = Utils.LoadSprite(item.IconImagePath);
}
}
/// <summary>
/// 显示物品信息
/// </summary>
/// <param name="eventData"></param>
public void OnPointerExit(PointerEventData eventData)
{
if (item.Amount!=0)
itemInfo.SetActive(false);
}
/// <summary>
/// 关闭物品信息
/// </summary>
/// <param name="eventData"></param>
public void OnPointerEnter(PointerEventData eventData)
{
if (item.Amount!=0)
itemInfo.SetActive(true);
}
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.button == PointerEventData.InputButton.Left && item.Id != 0)
leftClick.Invoke();
else if (eventData.button == PointerEventData.InputButton.Right && item.Id != 0)
rightClick.Invoke();
}
private void ButtonLeftClick()
{
switch(BagSlotsController.instance.mode)
{
case BagSlotsController.Mode.Use:
{
switch (item.Type)
{
case Item.ItemType.Consumble: Equip.instance.EquipConsumble(item); break;
case Item.ItemType.Weapon: Equip.instance.EquipWeapon(item); break;
case Item.ItemType.Armor: Equip.instance.EquipArmor(item); break;
default: break;
}
break;
}
case BagSlotsController.Mode.ForgeSelect:
{
ForgeController.instance.selectedSlot.item = Item.CopyItem(item);
BagManager.instance.RemoveItemFromBag(item, 1);
GameController.instance.SetGameObjectInactive(GameController.instance.bagPanel);
break;
}
}
}
private void ButtonRightClick()
{
switch (BagSlotsController.instance.mode)
{
case BagSlotsController.Mode.Use:
{
if (item.Type == Item.ItemType.Consumble)
Consume.instance.ConsumeItem(item);
else
TipController.instance.AddTip("当前物品无法使用", Tip.TipType.Middle);
break;
}
case BagSlotsController.Mode.ForgeSelect:
{
break;
}
}
}
}
BagSlotsController.cs
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 管理背包slotUI
/// </summary>
public class BagSlotsController: MonoBehaviour
{
public Mode mode;
public BagSlot[] slots;
public Text goldAmount;
public Text level;
public Text maxHP;
public Text maxMP;
public Text attack;
public Text defence;
public Text critChance;
public Text critRate;
public Text speed;
public static BagSlotsController instance;
public enum Mode
{
Use,
ForgeSelect
}
private void Start()
{
instance = this;
slots = GetComponentsInChildren<BagSlot>();
}
private void Update()
{
LoadBag();
ShowPlayerInfo();
}
/// <summary>
/// 根据BagManager中bag加载背包UI
/// </summary>
private void LoadBag()
{
goldAmount.text = ":" + BagManager.instance.money;
for (int i = 0; i < slots.Length; i++)
{
if (i < BagManager.instance.bag.items.Count)
slots[i].item = BagManager.instance.bag.items[i];
else
slots[i].item = new Item();
}
}
private void ShowPlayerInfo()
{
Value value = GameController.instance.GetPlayerState().value;
level.text = "等级:" + value.level;
maxHP.text = "血量上限:" + value.maxHP;
maxMP.text = "魔法上限:" + value.maxMP;
attack.text = "攻击力:" + value.attack;
defence.text = "防御力:" + value.defence;
critChance.text = "暴击率:" + value.critChance;
critRate.text = "暴击伤害:" + value.critRate;
speed.text = "速度:" + value.runSpeed;
}
public void ClosePanel()
{
foreach (BagSlot slot in slots)
{
slot.itemInfo.SetActive(false);
}
gameObject.SetActive(false);
}
}
SkillController.cs
使用类似简单工厂模式,对外接口EquipmentSkillMatch根据传入装备ID执行不同操作
using UnityEngine;
public class SkillController : MonoBehaviour
{
public static SkillController instance;
public GameObject fireCircle;
public GameObject cureCircle;
public GameObject lightingExplode;
public GameObject fireStorm;
private void Start()
{
instance = this;
}
public void CreateFireCircle(Vector3 position,string victimTag,State attackerState,float harmSpan,float baseHarm,float lastingTime)
{
GameObject obj = Instantiate(fireCircle,position,Quaternion.identity);
obj.GetComponent<FireCircle>().Initialize(victimTag, attackerState, harmSpan, baseHarm, lastingTime);
}
public void CreateCureCircle(Vector3 position, string receiverTag,float cureSpan, float baseCure,float extraCure,float lastingTime)
{
GameObject obj = Instantiate(cureCircle, position, Quaternion.identity);
obj.GetComponent<CureCircle>().Initialize(receiverTag, baseCure, extraCure, lastingTime, cureSpan);
}
public void CreateLightingExplode(Vector3 position, string victimTag, State attackerState, float baseHarm, float lastingTime)
{
GameObject obj = Instantiate(lightingExplode, position, Quaternion.identity);
obj.GetComponent<LightingExplode>().Initialize(victimTag, attackerState, baseHarm, lastingTime);
}
public void CreateFireStorm(Vector3 position,Vector3 moveDir, float moveSpeed, float attackSpan, float lastingTime, float baseharm, string victimTag, State attackerState)
{
GameObject obj = Instantiate(fireStorm, position, Quaternion.identity);
obj.GetComponent<FireStorm>().Initialize(moveDir,moveSpeed,attackSpan,lastingTime,baseharm,victimTag,attackerState);
}
/// <summary>
/// 根据不同装备id释放对应技能
/// </summary>
/// <param name="id"></param>
public bool EquipmentSkillMatch(int id)
{
switch(id)
{
case 9:CreateLightingExplode(GameController.instance.GetPlayerPosition(), Tags.enemy, GameController.instance.GetPlayerState(), 100, 0.5f);return true;
case 21:CreateFireCircle(GameController.instance.GetPlayerPosition()+new Vector3(0,0.5f,0), Tags.enemy, GameController.instance.GetPlayerState(), 0.5f, 60, 20);return true;
case 12:CreateFireStorm(GameController.instance.GetPlayerPosition(), GameController.instance.player.transform.forward, 2.0f, 0.3f, 5.0f, 50, Tags.enemy, GameController.instance.GetPlayerState());return true;
case 17:CreateCureCircle(GameController.instance.GetPlayerPosition(), Tags.player, 0.5f, 50, 0.03f, 10.0f);return true;
default:TipController.instance.AddTip("当前装备无技能", Tip.TipType.Quick);return false;
}
}
public string GetSkillImgPath(int id)
{
switch(id)
{
case 9:return "Textures/UI/SkillIcon/LightingExplode";
case 21:return "Textures/UI/SkillIcon/FireCircle";
case 12:return "Textures/UI/SkillIcon/FireStorm";
case 17:return "Textures/UI/SkillIcon/CureCircle";
default:return Path.DefaultImgPath;
}
}
}
Player.cs
using UnityEngine;
[System.Serializable]
public class PlayerInfo
{
public int exp;
public Vector3 pos;
public PlayerInfo(int exp, Vector3 pos)
{
this.exp = exp;
this.pos = pos;
}
}
public class Player : MonoBehaviour
{
private Transform camTransform;
public bool isRun;
public bool isAttack;
private Vector3 camForward; //临时三维坐标
public State state;
public int exp;
public float weaponSkillCD;
private float lastWeaponSkillTime=-100.0f;
public float leftWeaponSkillTime;
public float armorSkillCD;
private float lastArmorSkillTime=-100.0f;
public float leftArmorSkillTime;
private float lastAttackTime = -100.0f;
public float leftAttackTime;
public float blockSpan = 5.0f;
private float lastBlockTime=-100.0f;
public float leftBlockTime;
public float mpConsume = 10.0f;
public GameObject normalAttack;
public GameObject blockAttack;
public AudioClip[] clips;
private CharacterController cc;
private Animator anim;
private float timer;
private AudioSource baseAudio;//控制攻击以及翻滚
private AudioSource runAudio;//控制移动
void Start()
{
camTransform = Camera.main.transform;
state = GetComponent<State>();
cc = GetComponent<CharacterController>();
anim = GetComponent<Animator>();
baseAudio = GetComponents<AudioSource>()[0];
runAudio = GetComponents<AudioSource>()[1];
if (SceneController.instance != null && !SceneController.instance.isNewGame)
LoadPlayerInfo();
}
void Update()
{
UpdateValue();
timer += Time.deltaTime;
#region 更新技能CD
leftWeaponSkillTime = (weaponSkillCD - timer + lastWeaponSkillTime<0)?0: weaponSkillCD - timer + lastWeaponSkillTime;
leftArmorSkillTime = (armorSkillCD - timer + lastArmorSkillTime < 0) ? 0 : armorSkillCD - timer + lastArmorSkillTime;
leftAttackTime = (state.value.attackSpan - timer + lastAttackTime < 0) ? 0 : state.value.attackSpan - timer + lastAttackTime;
leftBlockTime = (blockSpan - timer + lastBlockTime < 0) ? 0 : blockSpan - timer + lastBlockTime;
#endregion
#region 连招攻击
if (Utils.GetKeyDown(KeyCode.J,SettingManager.instance.inputInfo.AttackKey))
{
if ((anim.GetCurrentAnimatorStateInfo(0).IsName("Idle")||anim.GetCurrentAnimatorStateInfo(0).IsName("Run"))&&timer-lastAttackTime>state.value.attackSpan)
{
anim.SetTrigger("attack1");
lastAttackTime = timer;
}
if (combo == 1)
{
if (anim.GetCurrentAnimatorStateInfo(0).IsName("Attack1"))
{
anim.SetTrigger("attack2");
}
if (anim.GetCurrentAnimatorStateInfo(0).IsName("Attack2"))
{
anim.SetTrigger("attack3");
}
anim.SetBool("attackFinish", false);
lastAttackTime = timer;
}
}
if(combo==0)
{
anim.SetBool("attackFinish", true);
}
#endregion
#region 翻滚
if (Utils.GetKeyDown(KeyCode.LeftShift,SettingManager.instance.inputInfo.RollKey)&& anim.GetCurrentAnimatorStateInfo(0).IsName("Run"))
{
anim.SetTrigger("roll");
baseAudio.clip = clips[3];
baseAudio.Play();
}
if (isRoll == 1)
{
//翻滚处理
}
#endregion
#region 格挡
if (Utils.GetKeyDown(KeyCode.H,SettingManager.instance.inputInfo.BlockKey) && (anim.GetCurrentAnimatorStateInfo(0).IsName("Run") || anim.GetCurrentAnimatorStateInfo(0).IsName("Idle") || anim.GetCurrentAnimatorStateInfo(0).IsName("Roll")))
{
if(timer - lastBlockTime > blockSpan)
{
lastBlockTime = timer;
anim.SetTrigger("block");
anim.SetBool("blockIdle", true);
}
else
TipController.instance.AddTip("格挡技能冷却中", Tip.TipType.Quick);
}
if((Input.GetKeyUp(KeyCode.H)||SettingManager.instance==null)&&Input.GetKeyUp(SettingManager.instance.inputInfo.BlockKey))
{
anim.SetBool("blockIdle", false);
}
if (anim.GetCurrentAnimatorStateInfo(0).IsName("Block") && state.isAttacked)
{
lastBlockTime = -100.0f;
state.isAttacked = false;
state.harm = 0;
blockAttack.GetComponent<BlockAttack>().isActive = true;
baseAudio.clip = clips[4];
baseAudio.Play();
Debug.Log("success");
}
if(anim.GetCurrentAnimatorStateInfo(0).IsName("BlockIdle")&& state.isAttacked)
{
state.isAttacked = false;
state.harm = 0;
}
#endregion
#region 受击
if(state.isAttacked)
{
baseAudio.clip = clips[5];
baseAudio.Play();
Attacked();
}
#endregion
#region 武器技能
if(Utils.GetKeyDown(KeyCode.E,SettingManager.instance.inputInfo.WeaponSkillKey)&& BagManager.instance != null)
{
if (timer - lastWeaponSkillTime > weaponSkillCD)
{
if (state.value.presentMP >= mpConsume)
{
if (SkillController.instance.EquipmentSkillMatch(BagManager.instance.weaponSlot.item.Id))
{
state.value.presentMP -= mpConsume;
lastWeaponSkillTime = timer;
}
}
else
TipController.instance.AddTip("MP剩余不足", Tip.TipType.Middle);
}
else
TipController.instance.AddTip("武器技能冷却中", Tip.TipType.Middle);
}
#endregion
#region 防具技能
if(Utils.GetKeyDown(KeyCode.R,SettingManager.instance.inputInfo.ArmorSkillKey)&& BagManager.instance != null)
{
if (timer - lastArmorSkillTime > armorSkillCD)
{
if(state.value.presentMP>=mpConsume)
{
if (SkillController.instance.EquipmentSkillMatch(BagManager.instance.armorSlot.item.Id))
{
state.value.presentMP -= mpConsume;
lastArmorSkillTime = timer;
}
}
else
{
TipController.instance.AddTip("MP剩余不足", Tip.TipType.Middle);
}
}
else
TipController.instance.AddTip("防具技能冷却中", Tip.TipType.Middle);
}
#endregion
#region 使用药品
if (Input.GetKeyDown(KeyCode.T))
{
}
#endregion
#region 重生检测
if (state.value.presentHP <= 0)
Reborn();
#endregion
}
// Update is called once per frame
void FixedUpdate()
{
Run();
}
public void TransmitToPos(Vector3 pos)
{
if(SceneController.instance!=null)
SceneController.instance.ShowLoadingCanva();
cc.enabled = false;
transform.position = pos;
cc.enabled = true;
}
private void Reborn()
{
TransmitToPos(GameController.instance.waterFountainPos);
state.value.presentHP = state.value.maxHP;
state.value.presentMP = state.value.maxMP;
}
#region 移动控制函数
private void Run()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
if ((Mathf.Abs(h) > 0.1f || Mathf.Abs(v) > 0.1f)&&state.canMove)
{
anim.SetBool("run", true);
isRun = true;
if (anim.GetCurrentAnimatorStateInfo(0).IsName("Run") || anim.GetCurrentAnimatorStateInfo(0).IsName("Idle"))
{
if (!runAudio.isPlaying)
runAudio.Play();
cc.Move(camTransform.right * h * state.value.runSpeed * Time.deltaTime + camForward * v * state.value.runSpeed * Time.deltaTime);
//水平垂直方向系数不为0表示需要进行旋转
if (h != 0 || v != 0)
{
Rotating(h, v);
}
}
}
else
{
anim.SetBool("run", false);
isRun = false;
runAudio.Pause();
}
}
void Rotating(float hh, float vv)
{
camForward = Vector3.Cross(camTransform.right, Vector3.up);
Vector3 targetDir = camTransform.right * hh + camForward * vv;
Quaternion targetRotation = Quaternion.LookRotation(targetDir, Vector3.up);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, state.value.rotateSpeed * Time.deltaTime);
}
#endregion
#region 动画事件函数
private int isRoll;//为1时表示当前处于翻滚
private void SetRollState(int state)
{
isRoll = state;
}
public int combo;//为1时可以进行连击
void SetComboState(int state)
{
combo = state;
}
#endregion
#region 攻击事件函数
void Attack1Start()
{
normalAttack.GetComponent<NormalAttack>().harm = 1;
normalAttack.SetActive(true);
baseAudio.clip = clips[0];
baseAudio.Play();
Debug.Log("attack1");
}
void Attack2Start()
{
normalAttack.GetComponent<NormalAttack>().harm = 2;
normalAttack.SetActive(true);
baseAudio.clip = clips[1];
baseAudio.Play();
Debug.Log("attack2");
}
void Attack3Start()
{
normalAttack.GetComponent<NormalAttack>().harm = 3;
normalAttack.SetActive(true);
baseAudio.clip = clips[2];
baseAudio.Play();
Debug.Log("attack3");
}
void AttackEnd()
{
normalAttack.SetActive(false);
isAttack = false;
}
void Attacked()
{
state.isAttacked = false;
state.value.presentHP -= state.harm;
state.harm = 0;
Debug.Log("Player is Attacked");
anim.SetTrigger("attacked");
GameController.instance.ShakeCamera();
}
#endregion
#region 数值控制函数
private void UpdateValue()
{
Value value = state.value;
Utils.SetBaseValueByLevel(value);
if (BagManager.instance != null)
{
Item weapon = (BagManager.instance.weaponSlot.item != null) ? BagManager.instance.weaponSlot.item : new Item();
Item armor = (BagManager.instance.armorSlot.item != null) ? BagManager.instance.armorSlot.item : new Item();
Item consumble = (BagManager.instance.consumbleSlot.item != null) ? BagManager.instance.consumbleSlot.item : new Item();
value.attack = value.baseAttack + weapon.Attack + armor.Attack + consumble.Attack + BuffController.instance.tempAttack;
value.defence = value.baseDefence + weapon.Defense + armor.Defense + consumble.Defense + BuffController.instance.tempDefence;
value.critChance = value.baseCritChance + weapon.CritChance + armor.CritChance + consumble.CritChance + BuffController.instance.tempCritChance;
value.critRate = value.baseCritRate + weapon.CritRate + armor.CritRate + consumble.CritRate + BuffController.instance.tempCritRate;
}
else
{
value.attack = value.baseAttack + BuffController.instance.tempAttack;
value.defence = value.baseDefence + BuffController.instance.tempDefence;
value.critChance = value.baseCritChance + BuffController.instance.tempCritRate;
value.critRate = value.baseCritRate + BuffController.instance.tempCritRate;
}
value.maxHP = value.baseHP+BuffController.instance.tempMaxHp;
value.maxMP = value.baseMP+BuffController.instance.tempMaxMp;
value.damageIncrease = BuffController.instance.tempHarmIncrease;
value.damageReduction = BuffController.instance.tempHarmReduction;
value.presentHP = (value.presentHP > value.maxHP) ? value.maxHP : value.presentHP;
value.presentHP = (value.presentHP < 0) ? 0 : value.presentHP;
value.presentMP = (value.presentMP > value.maxMP) ? value.maxMP : value.presentMP;
value.presentMP = (value.presentMP < 0) ? 0 : value.presentMP;
}
#endregion
#region 存档控制函数
public void SavePlayerInfoToJson()
{
PlayerInfo playerInfo = new PlayerInfo(exp, transform.position);
Utils.WriteJsonData(playerInfo, Path.PlayerInfoPath);
}
public void LoadPlayerInfo()
{
string jsonData = Utils.GetJsonData(Path.PlayerInfoPath);
PlayerInfo playerInfo= JsonUtility.FromJson<PlayerInfo>(jsonData);
exp = playerInfo.exp;
TransmitToPos(playerInfo.pos);
}
#endregion
}
GameController.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using static UnityEditor.Experimental.GraphView.GraphView;
public class GameController : MonoBehaviour
{
public static GameController instance;
public GameObject player;
public ItemDetect itemDetect;
public Image weaponSkillMask;
public Image weaponSkillImg;
public Text weaponSkillLeftTime;
public Image armorSkillMask;
public Image armorSkillImg;
public Text armorSkillLeftTime;
public Image attackMask;
public Text attackLeftTime;
public Image blockMask;
public Text blockLeftTime;
public Slider hpBar;
public Text hpText;
public Slider mpBar;
public Text mpText;
public Slider expBar;
public Text expText;
public GameObject common;
public GameObject uncommon;
public GameObject rare;
public GameObject epic;
public GameObject legendary;
public GameObject artifact;
public GameObject bagPanel;
public GameObject pickPanel;
public GameObject shopPanel;
public GameObject dialogPanel;
public GameObject taskPanel;
public GameObject forgePanel;
public GameObject notifyPanel;
public GameObject gamblingPanel;
public Vector3 waterFountainPos;
public bool shopPanelActive;
public bool forgePanelActive;
public bool gamblingPanelActive;
float shakeTime = 1.0f;//震动时间
private float currentTime = 0.0f;
private List<Vector3> gameobjpons = new List<Vector3>();
public Camera shakeCamera;//要求震动的相机
// Start is called before the first frame update
void Start()
{
instance = this;
}
private void Update()
{
//更新状态
player = GameObject.Find("Player");
UpdateMpBar();
UpdateHpBar();
UpdateExpBar();
UpdateSkillCD();
//打开背包
if(Utils.GetKeyDown(KeyCode.B,SettingManager.instance.inputInfo.BagKey))
{
SetGameObjectActive(bagPanel);
Invoke("SetBagModeUse", 0.1f);
}
//打开任务
if(Utils.GetKeyDown(KeyCode.V,SettingManager.instance.inputInfo.TaskKey))
{
SetGameObjectActive(taskPanel);
}
//拾取界面
if (itemDetect.nearItemObjs.Count > 0)
SetGameObjectActive(pickPanel);
else
SetGameObjectInactive(pickPanel);
//F键交互
if(Utils.GetKeyDown(KeyCode.F,SettingManager.instance.inputInfo.InterActKey))
{
if(shopPanelActive)
SetGameObjectActive(shopPanel);
if (forgePanelActive)
SetGameObjectActive(forgePanel);
if (gamblingPanelActive)
SetGameObjectActive(gamblingPanel);
}
}
void LateUpdate() { UpdateShake(); }
/// <summary>
/// 返回当前玩家位置
/// </summary>
/// <returns></returns>
public Vector3 GetPlayerPosition()
{
if (player == null)
return Vector3.zero;
else
return player.transform.position;
}
public Transform GetPlayerTransform()
{
return player.transform;
}
public State GetPlayerState()
{
return player.GetComponent<State>();
}
private void UpdateShake()
{
if (currentTime > 0.0f)
{
currentTime -= Time.deltaTime;
shakeCamera.rect = new Rect(0.04f * (-1.0f + 2.0f * Random.value) * Mathf.Pow(currentTime, 2), 0.04f * (-1.0f + 2.0f * Random.value) * Mathf.Pow(currentTime, 2), 1.0f, 1.0f);
}
else
{
currentTime = 0.0f;
}
}
public void ShakeCamera()
{
currentTime = shakeTime;
}
public void UpdateHpBar()
{
hpBar.value = GetPlayerState().value.presentHP / GetPlayerState().value.maxHP;
hpText.text = (int)GetPlayerState().value.presentHP+" / "+ (int)GetPlayerState().value.maxHP;
}
public void UpdateMpBar()
{
mpBar.value = GetPlayerState().value.presentMP / GetPlayerState().value.maxMP;
mpText.text = (int)GetPlayerState().value.presentMP + " / " + (int)GetPlayerState().value.maxMP;
}
public void UpdateExpBar()
{
int[] info= Utils.GetLevelByExp(player.GetComponent<Player>().exp);
int level = info[0];
int exp = info[1];
GetPlayerState().value.level = level;
expBar.value = exp / Utils.GetExpByLevel(level+1);
expText.text = exp + "/" + Utils.GetExpByLevel(level+1);
}
public void UpdateSkillCD()
{
//普通攻击
attackMask.fillAmount = player.GetComponent<Player>().leftAttackTime / GetPlayerState().value.attackSpan;
if (player.GetComponent<Player>().leftAttackTime != 0)
attackLeftTime.text = player.GetComponent<Player>().leftAttackTime.ToString("0.0") + "S";
else
attackLeftTime.text = " ";
//格挡
blockMask.fillAmount = player.GetComponent<Player>().leftBlockTime / player.GetComponent<Player>().blockSpan;
if (player.GetComponent<Player>().leftBlockTime != 0)
blockLeftTime.text = player.GetComponent<Player>().leftBlockTime.ToString("0.0") + "S";
else
blockLeftTime.text = " ";
//武器技能
weaponSkillImg.sprite = Utils.LoadSprite(SkillController.instance.GetSkillImgPath(BagManager.instance.weaponSlot.item.Id));
weaponSkillMask.fillAmount = player.GetComponent<Player>().leftWeaponSkillTime / player.GetComponent<Player>().weaponSkillCD;
if (player.GetComponent<Player>().leftWeaponSkillTime != 0)
weaponSkillLeftTime.text = player.GetComponent<Player>().leftWeaponSkillTime.ToString("0.0") + "S";
else
weaponSkillLeftTime.text = " ";
//防具技能
armorSkillImg.sprite = Utils.LoadSprite(SkillController.instance.GetSkillImgPath(BagManager.instance.armorSlot.item.Id));
armorSkillMask.fillAmount = player.GetComponent<Player>().leftArmorSkillTime / player.GetComponent<Player>().armorSkillCD;
if (player.GetComponent<Player>().leftArmorSkillTime != 0)
armorSkillLeftTime.text = player.GetComponent<Player>().leftArmorSkillTime.ToString("0.0") + "S";
else
armorSkillLeftTime.text = " ";
}
public void CreateItems(Item item,Vector3 position)
{
GameObject obj=null;
switch (item.Quality)
{
case Item.ItemQuality.Common:
obj = Instantiate(common, position, Quaternion.identity);
break;
case Item.ItemQuality.Uncommon:
obj = Instantiate(uncommon, position, Quaternion.identity);
break;
case Item.ItemQuality.Rare:
obj = Instantiate(rare, position, Quaternion.identity);
break;
case Item.ItemQuality.Epic:
obj = Instantiate(epic, position, Quaternion.identity);
break;
case Item.ItemQuality.Legendary:
obj = Instantiate(legendary, position, Quaternion.identity);
break;
case Item.ItemQuality.Artifact:
obj = Instantiate(artifact, position, Quaternion.identity);
break;
default:
break;
}
obj.GetComponent<ItemObject>().item = item;
}
public void SetGameObjectActive(GameObject obj)
{
obj.SetActive(true);
}
public void SetGameObjectInactive(GameObject obj)
{
obj.SetActive(false);
}
public void ShowDialogPanel(string path)
{
player.GetComponent<Player>().state.canMove = false;
dialogPanel.GetComponent<DialogController>().LoadDialogInfo(path);
dialogPanel.SetActive(true);
}
private void SetBagModeUse()
{
BagSlotsController.instance.mode = BagSlotsController.Mode.Use;
}
public void SaveAll()
{
player.GetComponent<Player>().SavePlayerInfoToJson();
BagManager.instance.SaveBagToJson();
BagManager.instance.SaveEquipmentToJson();
BagManager.instance.SaveMoneyToJson();
TaskController.instance.SaveTaskToJson();
}
}
Utils.cs
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using static Reward;
public class Utils
{
public static int maxLevel = 100;
/// <summary>
/// 返回值位于min与max包含两端的随机整数
/// </summary>
/// <param name="min"></param>
/// <param name="max"></param>
/// <returns></returns>
public static int GetRandomIntInRange(int min,int max)
{
System.Random random = new System.Random();
return random.Next(min, max+1);
}
/// <summary>
/// 从json文件中读取字符串
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static string GetJsonData(string path)
{
string readData;
using (StreamReader sr = File.OpenText(path))
{
//数据保存
readData = sr.ReadToEnd();
sr.Close();
}
return readData;
}
/// <summary>
/// 向json文件中写入类
/// </summary>
/// <param name="obj"></param>
/// <param name="path"></param>
public static void WriteJsonData(object obj,string path)
{
string js = UnityEngine.JsonUtility.ToJson(obj);
//打开或者新建文档
using (StreamWriter sw = new StreamWriter(path))
{
//保存数据
sw.WriteLine(js);
//关闭文档
sw.Close();
sw.Dispose();
}
}
/// <summary>
/// 加载指定路径sprite
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static Sprite LoadSprite(string path)
{
Texture2D texture1;
texture1 = Resources.Load<Texture2D>(path);
Sprite sprite = Sprite.Create(texture1, new Rect(0, 0, texture1.width, texture1.height), new Vector2(0.5f, 0.5f));
return sprite;
}
/// <summary>
/// 根据物品品质设置文本颜色
/// </summary>
/// <param name="_text"></param>
/// <param name="quality"></param>
public static void SetTextColorByQuality(Text _text, Item.ItemQuality quality)
{
switch (quality)
{
case Item.ItemQuality.Common:
_text.color = Color.white;
break;
case Item.ItemQuality.Uncommon:
_text.color = Color.green;
break;
case Item.ItemQuality.Rare:
_text.color = Color.blue;
break;
case Item.ItemQuality.Epic:
_text.color = Color.magenta;
break;
case Item.ItemQuality.Legendary:
_text.color = Color.yellow;
break;
case Item.ItemQuality.Artifact:
_text.color = Color.red;
break;
case Item.ItemQuality.Undroppable:
_text.color = Color.red;
break;
default:
break;
}
}
/// <summary>
/// 将item数组按类型排序
/// </summary>
/// <param name="items">待排序数组</param>
/// <param name="type">该类型位于数组前面</param>
public static void SortItemListByType(List<Item> items,Item.ItemType type)
{
Item temp;
for(int i=0;i<items.Count;i++)
{
if (items[i].Type!=type)
{
int j;
for (j = i + 1; j < items.Count; j++)
{
if (items[j].Type==type)
{
temp = items[i];
items[i] = items[j];
items[j] = temp;
break;
}
}
if (j == items.Count-1)
break;
}
}
}
public static Item.ItemType ChangeIntToType(int number)
{
switch(number)
{
case 0: return Item.ItemType.Consumble;
case 1: return Item.ItemType.Weapon;
case 2: return Item.ItemType.Armor;
case 3: return Item.ItemType.Material;
default:
Debug.LogError("参数匹配错误");
return Item.ItemType.Consumble;
}
}
/// <summary>
/// 造成伤害
/// </summary>
/// <param name="baseHarm">伤害数值</param>
/// <param name="attackerState">攻击者</param>
/// <param name="victimState">被攻击者</param>
public static float Harm(float baseHarm,State attackerState,State victimState)
{
float k1 = 50, k2 = 200;
float x = victimState.value.defence / (victimState.value.defence + victimState.value.level * k1 + k2);
victimState.isAttacked = true;
float critFactor = (GetRandomIntInRange(0, 100) < attackerState.value.critChance*100) ? 1 + attackerState.value.critRate : 1;
float harm = baseHarm * critFactor * (1 + attackerState.value.damageIncrease) * (1 - victimState.value.damageReduction) * (1 - x) * attackerState.value.attack;
victimState.harm += (harm > 0) ? harm : 0;
return harm;
}
/// <summary>
/// 治疗
/// </summary>
/// <param name="baseCure">基础治疗数值</param>
/// <param name="extraCure">额外最大生命百分比治疗量</param>
/// <param name="receiver">接受者</param>
public static void Cure(float baseCure,float extraCure,State receiver)
{
receiver.value.presentHP += baseCure + extraCure * receiver.value.maxHP;
}
/// <summary>
/// 根据等级设定人物基础数值
/// </summary>
/// <param name="value">人物value</param>
public static void SetBaseValueByLevel(Value value)
{
int x = value.level;
value.baseHP = 100 + 5 * x + x / 10 * 20;
value.baseMP = 80 + 4 * x + x / 10 * 10;
value.baseAttack = 30 + 3 * x + x / 10 * 10;
value.baseDefence = 10 + x + x / 10 * 5;
value.baseCritRate = 0.5f;
value.baseCritChance = 0.05f;
}
public static Vector3 GetRandomPositionAroundCenter(float maxDistance,Vector3 center)
{
float y = center.y;
float distance = maxDistance * GetRandomIntInRange(0, 100) / 100;
float angle = GetRandomIntInRange(0, 360);
float x = center.x + distance * Mathf.Cos(angle/3.14f*180);
float z = center.z + distance * Mathf.Sin(angle/3.14f*180);
return new Vector3(x, y, z);
}
public static string ChangeQualityToText(RewardQuality quality)
{
switch (quality)
{
case RewardQuality.Common: return "常见的";
case RewardQuality.Uncommon: return "稀有的";
case RewardQuality.Rare: return "罕见的";
case RewardQuality.Epic: return "史诗的";
default: return null;
}
}
/// <summary>
/// 根据人物当前经验返回人物等级以及溢出经验
/// </summary>
/// <param name="exp"></param>
/// <returns>等级,溢出经验</returns>
public static int[] GetLevelByExp(int exp)
{
int level;
for(level=0;level<maxLevel;level++)
{
exp -= GetExpByLevel(level);
if(exp<0)
{
exp += GetExpByLevel(level);
level--;
break;
}
}
return new int[2] { level, exp };
}
/// <summary>
/// 从level-1升级到level所需要经验
/// </summary>
/// <param name="level">等级</param>
/// <returns></returns>
public static int GetExpByLevel(int level)
{
return level * 100;
}
/// <summary>
/// 读取键盘输入
/// </summary>
/// <param name="keyCode">键盘key值</param>
/// <param name="keyCode1">SettingManager对应值</param>
/// <returns></returns>
public static bool GetKeyDown(KeyCode keyCode,KeyCode keyCode1)
{
return (Input.GetKeyDown(keyCode) && SettingManager.instance == null) || Input.GetKeyDown(keyCode1);
}
}
九、游戏系统测试
9.1单元测试
编程时对各个模块其中包括(人物行为模块,怪物行为模块,物品系统,背包系统,任务系统,拾取模块,合成系统,游戏系统,设置模块,场景切换模块等)进行测试,均无影响游戏正常运行的bug存在后再进行后续开发
9.2系统测试
原型设计完毕后多次使用不同测试人员进行测试均未发现严重影响游戏运行的bug
开始界面
设置界面
介绍界面
加载界面
游戏主界面
对话界面
商店界面
任务界面
合成界面
下注界面
战斗界面
拾取界面
背包界面
补充:
注意游戏在编辑器启动时从StartScence开始运行,直接从SampleScence运行会报空指针错误,(因为一些全局控件是公用的在切换场景时复用),2025/5/25亲测能够正常运行。