今天打开我尘封的项目,发现我居然有点看不懂了 而且我的专业版许可证退化成了个人版 (⊙︿⊙) 太可恶了(不过影响不亚于没吃早饭)
没办法,重新捋一遍吧,用进废退,不用就会遗忘如是说也
Scene场景
当时我是创建了四个场景,TheBeginingScene(主场景),TVGame(小游戏选取界面),EarthingHitting(第一个小游戏),GentlePigs(第二个游戏)
PokerCrush(第三个游戏)
众所周知,unity中的scene场景文件互不干扰
TheBeginingScene(主场景) 所以TheBeginingScene(主场景)中的Hierarchy(层级)窗口里重新熟悉一下之前创建的场景树吧
这里主要说一下这个小角色,这个是当时跟着教程制作的半成品
其中,player是父对象,Pistol和shootGun这两把枪是子对象
这里默认将两把枪对象禁用,注意,禁用父对象同时会禁用父对象底下的所有子对象
muzzle是枪口位置,bulletshell是开枪后弹壳生成的位置
这个我目前就放了transform组件用于定位子对象相对于父对象的local坐标系的位置
角色的动画状态机如下
分为那两个状态:
knight_idle和knight_run
状态也非常简单就两个,所以没有必要使用blend混合树(我之前 用混合树差点把我绕晕)
判断状态切换条件是布尔类型值isMoving,这个东西代码里会设置的
建立状态之间的连接 make transition
刚才忘记说了,在player父对象上需要增加这几个组件
transform转换组件——控制角色的位置,旋转,缩放
sprite renderer 精灵图渲染组件——-渲染精灵图到游戏画面
animator—–动画组件,用于放置角色的行为控制组件animate controller
rigidbody 2d ——2d刚体组件,控制角色的重力,默认gravity scale为0,如果大于0游戏开始时角色会获得重力
至于下面的这个是角色的C#控制脚本,控制角色的行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 using System.Collections;using System.Collections.Generic;using UnityEngine;public class Player_Controller : MonoBehaviour { public float playerSpeed = 5f ; private Rigidbody2D playerRigidbody; private Animator playerAnimator; private Vector2 movementInput; public GameObject[] weapons; private int currentWeaponIndex; private Vector2 mousePosition; void Start () { GetComponents(); weapons[0 ].SetActive(true ); } void Update () { MovementLogic(); SwitchWeapon(); } void GetComponents () { playerRigidbody = GetComponent<Rigidbody2D>(); playerAnimator = GetComponent<Animator>(); } void MovementLogic () { movementInput.x = Input.GetAxisRaw("Horizontal" ); movementInput.y = Input.GetAxisRaw("Vertical" ); playerRigidbody.velocity = movementInput.normalized * playerSpeed; mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition); if (mousePosition.x > transform.position.x) { transform.rotation = Quaternion.Euler(new Vector3(0 , 0 , 0 )); } else { transform.rotation = Quaternion.Euler(new Vector3(0 , 180 , 0 )); } if (movementInput != Vector2.zero) { playerAnimator.SetBool("isMoving" , true ); } else { playerAnimator.SetBool("isMoving" , false ); } } void SwitchWeapon () { if (Input.GetKeyDown(KeyCode.Q)) { weapons[currentWeaponIndex].SetActive(false ); if (--currentWeaponIndex < 0 ) { currentWeaponIndex = weapons.Length - 1 ; } weapons[currentWeaponIndex].SetActive(true ); } if (Input.GetKeyDown(KeyCode.E)) { weapons[currentWeaponIndex].SetActive(false ); if (++currentWeaponIndex > weapons.Length - 1 ) { currentWeaponIndex = 0 ; } weapons[currentWeaponIndex].SetActive(true ); } } }
这里就是首先Player_Controller 类继承 MonoBehaviour类(也就是unity自己定义的Start() Update() Awake()这些函数 )
这里定义了角色的很多属性,对了,unity中如果你在inspector检查器窗口给角色添加组件就必须在C#脚本中声明
比如就代表刚才的我们选择的刚体rigidbody2d组件 animator组件 还有Transform组件里的Vector2类,就是获得二维xy坐标值
对了,unity中的属性如果不加public 或者protected声明的话属性默认是私有的,私有就是这个属性只能在这个脚本里使用,如果是public公开的话其他脚本可以这样启用 首先看看有没有命名空间namespace定义,有的话就using这个命名空间,没有的话直接使用Player_Controller.playerSpeed获取这个属性
1 2 3 private Rigidbody2D playerRigidbody;private Animator playerAnimator;private Vector2 movementInput;
在游戏开始时我们需要让玩家角色获取组件(因为刚才只是定义而不是启用),为了保持start函数的干净整洁我把获取的行为放到GetComponents自定义函数里然后在start里调用这个函数
这里角色的行为逻辑由Update和Fixupdate控制,Update是按帧刷新界面,FixUpdate是按固定时间刷新界面,
而且Update主要控制角色的输入行为,FixUpdate控制物理逻辑
这里你观察MovementLogic和SwitchWeapon函数就可以发现主要是键盘的输入输出行为所以我们使用Update 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void MovementLogic () { movementInput.x = Input.GetAxisRaw("Horizontal" ); movementInput.y = Input.GetAxisRaw("Vertical" ); playerRigidbody.velocity = movementInput.normalized * playerSpeed; mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition); if (mousePosition.x > transform.position.x) { transform.rotation = Quaternion.Euler(new Vector3(0 , 0 , 0 )); } else { transform.rotation = Quaternion.Euler(new Vector3(0 , 180 , 0 )); } if (movementInput != Vector2.zero) { playerAnimator.SetBool("isMoving" , true ); } else { playerAnimator.SetBool("isMoving" , false ); } }
首先让玩家的输入的值x和y值分别为Horizontal(水平轴)和 Vertical(垂直轴)上输入返回值
Horizontal 会返回A 和 D 和左箭头 和右箭头的值 默认值为1或-1
Vertical 会返回W 和S 还有上箭头和下箭头 默认值为1或-1
1 2 3 movementInput.x = Input.GetAxisRaw("Horizontal" ); movementInput.y = Input.GetAxisRaw("Vertical" );
1 2 playerRigidbody.velocity = movementInput.normalized * playerSpeed;
然后将返回的二维坐标normalized归一化(确保各个方向的x平方加y的平方始终为1,保证各个方向移动速度一样,防止角色在斜方向移动速度加快的问题)乘以角色定义的速度得到角色的移动速度,这里选择移动角色的刚体Rigidbody的velocity速度,即赋予刚体一个速度
然后在update里速度乘以帧时间,当按下wasd(用来决定移动方向)时,通过移动角色的刚体小人就可以移动了
之前我们定义了一个参数用来存储鼠标在游戏界面上的位置,并将鼠标实时输入到游戏界面(就是以摄像机可以看到的范围的中心建立坐标系,这个范围是实时变动的)中的位置返回鼠标的相对位置
1 2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
1 2 3 4 5 6 7 8 9 if (mousePosition.x > transform.position.x) { transform.rotation = Quaternion.Euler(new Vector3(0 , 0 , 0 )); } else { transform.rotation = Quaternion.Euler(new Vector3(0 , 180 , 0 )); }
然后判断这个鼠标的位置和角色的X轴的相对位置,因为我的小骑士角色默认的朝向是右边,所以,当鼠标的x轴位置大于角色的x轴的位置时不改变角色的旋转值,反之则使用四元数欧拉旋转将角色翻转180°
1 2 3 4 5 6 7 8 9 if (movementInput != Vector2.zero) { playerAnimator.SetBool("isMoving" , true ); } else { playerAnimator.SetBool("isMoving" , false ); }
还记得刚才说的isMoving吗,此时就排上用场了,判断角色走或者不走的状态
这个其实是在animator的参数面板parameters里新建的bool值,当然这个也是属于animator组件的属性
然后结合状态机里的conditions里的设置大家懂了吗(也就是说,unity中的状态虽然在编辑器里设置了,但是只有在C#脚本中定义触发条件了才会发挥作用)
之前我们说过武器的存在,这里先不说武器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void SwitchWeapon () { if (Input.GetKeyDown(KeyCode.Q)) { weapons[currentWeaponIndex].SetActive(false ); if (--currentWeaponIndex < 0 ) { currentWeaponIndex = weapons.Length - 1 ; } weapons[currentWeaponIndex].SetActive(true ); } if (Input.GetKeyDown(KeyCode.E)) { weapons[currentWeaponIndex].SetActive(false ); if (++currentWeaponIndex > weapons.Length - 1 ) { currentWeaponIndex = 0 ; } weapons[currentWeaponIndex].SetActive(true ); } }
Input.GetKeyDown 这个是获取键盘持续按下的信号,而其他的比如Input.GetKey是键盘按钮按一次的信号,Input.GetKeyUP是捕获按钮松开的那一刻的信号,这里的意思是当我按下Q或者E时可以切换武器数组的索引值来切换武器,因为我们在start里设置,角色默认在游戏开始时拿着的武器是索引为0的第一把武器 即
1 weapons[0 ].SetActive(true );
当按下Q时先将当前角色正在拿着的武器对象禁用然后判断当前的索引值的上一个索引是否小于0(主要是应对初始是角色手持装备索引为0的情况),比如我们最开始的武器索引值是0,但一共就两把枪,数组的长度为2,所以索引值等于数组的长度减一即获得倒数第一个装备,然后激活这个装备,形成循环,这样我一直按Q就能一直切换武器
同理,按E也是相同的逻辑,只是反过来了
然后只需要去inspector检查器窗口放入两把枪的对象即可
这个蓝色的我们管它叫做prefabs预制体,这个不说了
对了忘记说了,再次之前我们需要先制作动画剪辑片段,这个无法用文字描述,大概就是新建一个状态,然后把切割出来的精灵图序列帧拖进到这个动画状态机中形成animation clip剪辑片段
然后状态会有一个对应animation controller来控制相关的所有clip,而controller 是放到animator组件里启用
然后在状态机里把这个剪辑clip片段文件放入对应的状态中
ok主场景熟悉完了,接下来我们要熟悉的就是TVgame场景
明天继续,今天睡觉
TVGame(小游戏选取界面) OK,睡醒了,现在熟悉一下TVgame场景
首先我们需要了解一下UI系统
UI就是玩家与游戏交互的界面,unity中所有UI都是以canvas为父节点,包括界面上的Text文字和Button按钮都是
比如你玩王者荣耀,原神,明日方舟终末地,界面上的设置界面,人物的血条,还有技能按钮攻击按钮这些,都是UI
不过我对UI了解还是太少了,以后学一下更高级的UI吧,比如UI树,这种最直接的方法就是反编译别的unity游戏看看大佬的怎么做的
canvas是在摄像机边界右上角,非常巨大
这里我是放了两个canvas,分别是gamesetting canvas 和 gamebutton
也就是一个掌管游戏的设置界面,一个掌管交互按钮
先说这个gamesetting canvas 吧
哦对了有一个细节就是
UI这个缩放最好选择随屏幕大小缩放,不然窗口大小改变时会发生UI的变形,一般会选择窗口大小为1920×1080的大小
这里我使用了一个pannel,用来作为设置UI的父节点,也就是背后的白色面板
对了,这个是游戏背景音乐的设置脚本,然后需要更改对象物体的标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 using UnityEngine.UI;using UnityEngine;namespace MyAssets.Framework { public class VolumeController : MonoBehaviour { private AudioSource MenuAudio; public Slider VolumeSlider; void Start () { MenuAudio = GameObject.FindGameObjectWithTag("Menu" ).transform.GetComponent<AudioSource>(); VolumeSlider=GameObject.FindGameObjectWithTag("GameSetting" ).transform.GetComponent<Slider>(); } void Update () { VolumeControl(); } public void VolumeControl () { MenuAudio.volume = VolumeSlider.value ; } } }
为了实现Button的移动效果(说是button,其实适用于所有的游戏对象),我用AI做了这个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 using UnityEngine;namespace MyAssets.Framework { public class ButtonSlide : MonoBehaviour { public enum MoveDirection { Right, Left, Up, Down } [Header("移动模式选择" ) ] [Tooltip("是否使用自定义终点位置,如果勾选则禁用距离选项" )] public bool useCustomTarget = false ; [Header("自定义终点(当 useCustomTarget = true 时启用)" ) ] public Vector2 targetPosition; [Header("按方向与距离移动(当 useCustomTarget = false 时启用)" ) ] public MoveDirection direction = MoveDirection.Right; public float distance = 500f ; [Header("运动参数" ) ] [Tooltip("初速度(像素/秒)" )] public float initialSpeed = 800f ; [Tooltip("加速度(默认为0时自动计算,使末速度为0)" ) ] public float acceleration = 0f ; [Header("控制选项" ) ] public bool autoPlay = true ; public bool loop = false ; private RectTransform rect; private Vector2 startPos; private Vector2 endPos; private float currentSpeed; private float usedAcceleration; private bool isPlaying = false ; private float traveled = 0f ; private float totalDistance = 0f ; private void Awake () { rect = GetComponent<RectTransform>(); startPos = rect.anchoredPosition; if (useCustomTarget) { endPos = targetPosition; totalDistance = Vector2.Distance(startPos, endPos); } else { endPos = startPos + DirectionVector() * distance; totalDistance = distance; } if (acceleration == 0f ) { acceleration = -(initialSpeed * initialSpeed) / (2 * totalDistance); } usedAcceleration = acceleration; } private void Start () { if (autoPlay) Play(); } public void Play () { isPlaying = true ; traveled = 0f ; currentSpeed = initialSpeed; startPos = rect.anchoredPosition; if (useCustomTarget) { endPos = targetPosition; totalDistance = Vector2.Distance(startPos, endPos); } else { endPos = startPos + DirectionVector() * distance; totalDistance = distance; } if (acceleration == 0f ) { acceleration = -(initialSpeed * initialSpeed) / (2 * totalDistance); } usedAcceleration = acceleration; } private void Update () { if (!isPlaying) return ; float delta = currentSpeed * Time.deltaTime; traveled += Mathf.Abs(delta); if (traveled >= totalDistance) { rect.anchoredPosition = endPos; currentSpeed = 0f ; isPlaying = false ; if (loop) { (startPos, endPos) = (endPos, startPos); Play(); } return ; } Vector2 dir = (endPos - startPos).normalized; rect.anchoredPosition += dir * delta; currentSpeed += usedAcceleration * Time.deltaTime; if (currentSpeed < 0 ) currentSpeed = 0 ; } private Vector2 DirectionVector () { switch (direction) { case MoveDirection.Left: return Vector2.left; case MoveDirection.Right: return Vector2.right; case MoveDirection.Up: return Vector2.up; case MoveDirection.Down: return Vector2.down; default : return Vector2.right; } } public void Stop () => isPlaying = false ; #if UNITY_EDITOR private void OnValidate () { if (useCustomTarget) { distance = Mathf.Max(0.01f , distance); } } #endif } }
然后就是设定好移动速度和移动方向和移动距离,这里我是想让这个按钮向上移动
但是开始的时候我是把这个设置面板设置为false状态
当且仅当我点击这个设置按钮–也就是这个骰子ui按钮时,才会弹出来这个
就是在按钮的on click里选择Panel对象,点击事件为SetActive激活物品对象,这个还需要另一个脚本放到一个空对象上
来控制弹出的设置界面的关闭(说实在,感觉真的没必要,但是我没有更好的解决办法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using UnityEngine;namespace MyAssets.Framework { public class SetActiveObject : MonoBehaviour { public GameObject myObject; bool isActive = false ; void Update () { OpenObject(); } void OpenObject () { if (Input.GetKeyDown(KeyCode.Escape)) { isActive=!isActive; myObject.SetActive(isActive); } } } }
这里为了给按钮增加一个鼠标放上去按钮就会放大一点,离开就会恢复原状,这里增加一个通用脚本button scale
这里的命名空间MyAssets.Framework 是指所有对象都可以公用的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 using UnityEngine;using UnityEngine.EventSystems;using UnityEngine.UI;namespace MyAssets.Framework { public class ButtonScale : MonoBehaviour { [Header("按钮" ) ] public Button btn; [Header("放大设置" ) ] public float scaleMultiplier = 1.15f ; public float animSpeed = 8f ; protected Vector3 originalScale; protected bool isHovering; protected virtual void Awake () { originalScale = transform.localScale; btn = GetComponent<Button>(); if (btn == null ) { Debug.LogError("HoverScale 需要挂在 Button 物体上" ); } } protected virtual void OnEnable () { btn.onClick.AddListener(() => { }); var trigger = btn.gameObject.GetComponent<EventTrigger>(); if (trigger == null ) { trigger = btn.gameObject.AddComponent<EventTrigger>(); } var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerEnter }; entry.callback.AddListener(_ => isHovering = true ); trigger.triggers.Add(entry); var exit = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit }; exit.callback.AddListener(_ => isHovering = false ); trigger.triggers.Add(exit); } protected virtual void Update () { Vector3 target = isHovering ? originalScale * scaleMultiplier : originalScale; transform.localScale = Vector3.Lerp(transform.localScale, target, animSpeed * Time.deltaTime); } } }
说实话,我的逻辑十分混乱我把函数都作为 protected virtual属性目的就是为了实现可以继承,就是让startbutton和exitbutton继承按钮可以缩放的功能,现在看来真的是完全没有必要啊
结果导致屎山的出现
然后就导致接下来的两个脚本exitbutton和stratbutton离不开这个脚本,也就是必须继承这个脚本的属性,甚至为这个东西搞了一个接口ISceneLoader来切换场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using UnityEngine;using UnityEngine.UI;namespace MyAssets.Framework { public class ExitButton : ButtonScale , ISceneLoader { public string targetSceneName = "TVSelectionScene" ; private IButtonEscape escapeLogic; protected override void Awake () { base .Awake(); scaleMultiplier = 0.85f ; if (btn == null ) btn = GetComponent<Button>(); btn?.onClick.RemoveAllListeners(); btn?.onClick.AddListener(OnClick); escapeLogic = GetComponent<IButtonEscape>(); } void Update () { base .Update(); escapeLogic?.HandleEscape(transform as RectTransform, GetComponentInParent<Canvas>()); } private void OnClick () { btn.interactable = false ; ButtonManager.Instance.DisableOther(btn); SwitchScene(1f ); } public void SwitchScene (float delay = 0f ) { ButtonManager.Instance.LoadScene(targetSceneName, delay); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 using UnityEngine;using UnityEngine.UI;namespace MyAssets.Framework { public class StartButton : ButtonScale , ISceneLoader { public string targetSceneName = "EarthHittingGameScene" ; protected override void Awake () { base .Awake(); scaleMultiplier = 1.15f ; if (btn == null ) btn = GetComponent<Button>(); if (btn != null ) { btn.onClick.RemoveAllListeners(); btn.onClick.AddListener(OnClick); } else { Debug.LogError($"{name} : StartButton 缺少 Button 组件!" ); } } private void OnClick () { btn.interactable = false ; ButtonManager.Instance.DisableOther(btn); SwitchScene(1f ); } public void SwitchScene (float delay = 0f ) { ButtonManager.Instance.LoadScene(targetSceneName, delay); } } }
1 2 3 4 5 6 7 namespace MyAssets.Framework { public interface ISceneLoader { void SwitchScene (float delay = 0f ) ; } }
但是我目前没有更好的解决办法,还需要多看看大佬的教学,现在的代码完全是可以跑的屎山
unity中只有把场景scene文件添加到这里才能被编译器识别
我当时也是神经病,因为我是这样想的,开始按钮脚本startbutton放到这三个彩色的按钮上,然后退出这个场景(本质是切换场景)按这个装载了exitbutton的按钮,然后在每个按钮对象的Inventory检查器窗口里设置点击后要切换的目标场景也就是Target scene name,不过先需要指定当前的要被点击的按钮是哪一个按钮(怎么感觉有点小题大做)
这里开始按钮和退出按钮本质上没有区别,唯一的区别就是退出按钮增加一个逻辑就是退出按钮我是模仿米塔做的一个好玩的效果—-当我试图退出游戏时按钮会尝试逃离鼠标指针不让玩家退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 using UnityEngine;namespace MyAssets.Framework { [RequireComponent(typeof(RectTransform)) ] public class ButtonEscapeLogic : MonoBehaviour , IButtonEscape { [Header("功能开关" ) ] public bool enableEscape = true ; public bool EnableEscape { get => enableEscape; set => enableEscape = value ; } [Header("检测范围" ) ] public Vector2 detectBoxSize = new Vector2(300f , 150f ); public Vector2 DetectBoxSize { get => detectBoxSize; set => detectBoxSize = value ; } [Header("逃跑参数" ) ] public float escapeDistance = 200f ; public float EscapeDistance { get => escapeDistance; set => escapeDistance = value ; } public float moveSpeed = 6f ; public float MoveSpeed { get => moveSpeed; set => moveSpeed = value ; } [Header("可视化设置" ) ] public Color gizmoColor = new Color(0f , 191f , 255f , 0.3f ); private RectTransform rect; private Canvas canvas; private Vector2 originalPos; private Vector2 targetPos; void Awake () { rect = GetComponent<RectTransform>(); canvas = GetComponentInParent<Canvas>(); originalPos = rect.anchoredPosition; targetPos = originalPos; } void Update () { HandleEscape(rect, canvas); } public void HandleEscape (RectTransform rect, Canvas canvas ) { if (!enableEscape || !rect || !canvas) { return ; } Vector2 mousePos = Input.mousePosition; Vector2 buttonCenter = RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, rect.position); bool inside = mousePos.x >= buttonCenter.x - detectBoxSize.x / 2 && mousePos.x <= buttonCenter.x + detectBoxSize.x / 2 && mousePos.y >= buttonCenter.y - detectBoxSize.y / 2 && mousePos.y <= buttonCenter.y + detectBoxSize.y / 2 ; if (inside) { Vector2 dir = GetEscapeDirection(mousePos, buttonCenter); targetPos = originalPos + dir * escapeDistance; } else { targetPos = originalPos; } rect.anchoredPosition = Vector2.Lerp(rect.anchoredPosition, targetPos, moveSpeed * Time.deltaTime); } private Vector2 GetEscapeDirection (Vector2 mouse, Vector2 center ) { float dx = mouse.x - center.x; float dy = mouse.y - center.y; Vector2 dir = Vector2.zero; if (dx >= 0 && dy >= 0 ) dir = new Vector2(-1 , -1 ); else if (dx < 0 && dy >= 0 ) dir = new Vector2(1 , -1 ); else if (dx < 0 && dy < 0 ) dir = new Vector2(1 , 1 ); else if (dx >= 0 && dy < 0 ) dir = new Vector2(-1 , 1 ); if (Mathf.Approximately(dx, 0 )) dir.x = 0 ; if (Mathf.Approximately(dy, 0 )) dir.y = -Mathf.Sign(dy); return dir.normalized; } void OnDrawGizmos () { if (rect == null ) rect = GetComponent<RectTransform>(); if (rect == null ) return ; Vector3[] corners = new Vector3[4 ]; rect.GetWorldCorners(corners); Vector3 center = (corners[0 ] + corners[2 ]) / 2 ; Gizmos.color = gizmoColor; Gizmos.DrawWireCube(center, new Vector3(detectBoxSize.x, detectBoxSize.y, 0 )); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 using UnityEngine;namespace MyAssets.Framework { public interface IButtonEscape { bool EnableEscape { get ; set ; } Vector2 DetectBoxSize { get ; set ; } float EscapeDistance { get ; set ; } float MoveSpeed { get ; set ; } void HandleEscape (RectTransform rect, Canvas canvas ) ; } }
这里我也感觉和屎山一样,我是让这个检测范围可视化,这样方便我调整检测范围的大小,以及逃跑的距离
效果非常好是非常好,和米塔那个一模一样,但是我感觉自己脚本管理能力太差了,不能实现解耦合
这里游戏的背景我原本采用纯黑色,但是感觉太单调了,于是我在Pinterest上找灵感,然后找到了一种效果感觉非常好看,于是我的灵感是吧扑克牌的四种花色在asperite软件里画成这个效果,然后让这些图案随机移动
这里是通过一个脚本BackGroundItemController来实现这个扑克牌的四种花色的出现数量和一定范围还有移动逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 using System.Collections.Generic;using UnityEngine;namespace MyAssets.Framework { public class BackgroundItemController : MonoBehaviour { [Header("生成范围设置" ) ] public Vector2 areaSize = new Vector2(10f , 5f ); [Header("是否显示检测范围" ) ] public bool showArea = true ; [Header("范围颜色设置" ) ] public Color areaColor = new Color(0f , 1f , 0f , 0.2f ); [Header("预制体设置" ) ] public GameObject[] prefabs; [Range(0, 100) ] public int maxObjects = 10 ; [Header("移动速度随机范围" ) ] public float moveSpeedMin = 0.5f ; public float moveSpeedMax = 2f ; [Header("旋转速度随机范围" ) ] public float rotateSpeedMin = 10f ; public float rotateSpeedMax = 50f ; public float minDistanceBetweenObjects = 0.5f ; private List<GameObject> activeObjects = new List<GameObject>(); void Start () { while (activeObjects.Count < maxObjects) { SpawnRandomObject(); } } void Update () { for (int i = activeObjects.Count - 1 ; i >= 0 ; i--) { if (!activeObjects[i]) { activeObjects.RemoveAt(i); } else { Vector3 pos = activeObjects[i].transform.position; if (!IsInsideArea(pos)) { Destroy(activeObjects[i]); activeObjects.RemoveAt(i); } } } while (activeObjects.Count < maxObjects) { SpawnRandomObject(); } } void SpawnRandomObject () { if (prefabs == null || prefabs.Length == 0 ) { return ; } GameObject prefab = prefabs[Random.Range(0 , prefabs.Length)]; Vector2 spawnPos = GetRandomEdgePosition(); Vector3 worldPos = transform.position + new Vector3(spawnPos.x, spawnPos.y, 0 ); foreach (var obj in activeObjects) { if (!obj) { continue ; } if (Vector3.Distance(obj.transform.position, worldPos) < minDistanceBetweenObjects) { return ; } } GameObject newObj = Instantiate(prefab, worldPos, Quaternion.identity, transform); float randomAngle = Random.Range(0f , 360f ); newObj.transform.rotation = Quaternion.Euler(0 , 0 , randomAngle); BackgroundItemMover mover = newObj.AddComponent<BackgroundItemMover>(); mover.Init(this ); activeObjects.Add(newObj); } bool IsInsideArea (Vector3 pos ) { Vector3 localPos = pos - transform.position; return (Mathf.Abs(localPos.x) <= areaSize.x / 2f ) && (Mathf.Abs(localPos.y) <= areaSize.y / 2f ); } Vector2 GetRandomEdgePosition () { int edge = Random.Range(0 , 4 ); float x = 0 , y = 0 ; switch (edge) { case 0 : x = Random.Range(-areaSize.x / 2f , areaSize.x / 2f ); y = areaSize.y / 2f ; break ; case 1 : x = Random.Range(-areaSize.x / 2f , areaSize.x / 2f ); y = -areaSize.y / 2f ; break ; case 2 : x = -areaSize.x / 2f ; y = Random.Range(-areaSize.y / 2f , areaSize.y / 2f ); break ; case 3 : x = areaSize.x / 2f ; y = Random.Range(-areaSize.y / 2f , areaSize.y / 2f ); break ; } return new Vector2(x, y); } void OnDrawGizmos () { if (!showArea) { return ; } Gizmos.color = areaColor; Gizmos.DrawCube(transform.position, new Vector3(areaSize.x, areaSize.y, 0.01f )); } } }
然后又是屎山的出现,当时不知道怎么想的,把物品移动的逻辑单独分开了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using UnityEngine;namespace MyAssets.Framework { public class BackgroundItemMover : MonoBehaviour { private BackgroundItemController controller; private Vector3 direction; private float moveSpeed; private float rotateSpeed; private int rotateDir; public void Init (BackgroundItemController ctrl ) { controller = ctrl; direction = (ctrl.transform.position - transform.position).normalized + new Vector3(Random.Range(-0.3f , 0.3f ), Random.Range(-0.3f , 0.3f ), 0 ); moveSpeed = Random.Range(ctrl.moveSpeedMin, ctrl.moveSpeedMax); rotateSpeed = Random.Range(ctrl.rotateSpeedMin, ctrl.rotateSpeedMax); rotateDir = Random.value > 0.5f ? 1 : -1 ; } void Update () { transform.position += direction * (moveSpeed * Time.deltaTime); transform.Rotate(Vector3.forward, rotateDir * rotateSpeed * Time.deltaTime); } } }
然后我又干了一个傻逼的事,我给物品添加的碰撞逻辑,对,你没听错,碰撞逻辑,就是给物体添加碰撞体,还给这东西写了一套智能避障脚本,还搞了一个接口,纯纯屎山
结果一启动我的渣本卡的要死,现在已经考虑要不要去掉这个东西了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 using UnityEngine;namespace MyAssets.Framework { public class ItemElasticMover : MonoBehaviour { private Rigidbody2D rb; private BackgroundItemController controller; private float moveSpeed; private float rotateSpeed; private int rotateDir; [Header("检测与避让参数" ) ] [Tooltip("检测范围半径(单位:世界空间单位)" )] public float detectRadius = 1.5f ; [Tooltip("两次方向重置之间的冷却时间(秒)" ) ] public float resetCooldown = 0.5f ; [Tooltip("检测的层(默认全部)" ) ] public LayerMask detectionLayer = ~0 ; private float lastResetTime = -999f ; private float minSpeedThreshold = 0.05f ; [Header("检测范围可视化" ) ] public bool showDetectRange = true ; public Color detectRangeColor = new Color(1f , 0.5f , 0f , 0.3f ); public void Init (BackgroundItemController ctrl ) { controller = ctrl; rb = GetComponent<Rigidbody2D>(); rb.gravityScale = 0 ; rb.drag = 0.3f ; rb.angularDrag = 0.3f ; rb.mass = 1f ; rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; var col = GetComponent<Collider2D>(); var mat = new PhysicsMaterial2D("ElasticMat" ); mat.bounciness = 0.5f ; mat.friction = 0f ; col.sharedMaterial = mat; if (detectionLayer == 0 ) detectionLayer = LayerMask.GetMask("Default" ); ResetDirection(); } public void Move () { DetectAndReset(); MaintainMotion(); } private void DetectAndReset () { Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, detectRadius, detectionLayer); Vector2 separationForce = Vector2.zero; int nearbyCount = 0 ; foreach (var hit in hits) { if (hit.gameObject == gameObject) continue ; Vector2 diff = (Vector2)(transform.position - hit.transform.position); float dist = diff.magnitude; if (dist > 0.001f ) { separationForce += diff.normalized / dist; nearbyCount++; } } if (nearbyCount > 0 ) { separationForce /= nearbyCount; separationForce.Normalize(); separationForce += Random.insideUnitCircle * 0.3f ; separationForce.Normalize(); rb.AddForce(separationForce * (moveSpeed * 1.2f ), ForceMode2D.Impulse); if (rb.velocity.magnitude > controller.moveSpeedMax) rb.velocity = rb.velocity.normalized * controller.moveSpeedMax; } rb.angularVelocity = rotateSpeed * rotateDir; } private void ResetDirection () { if (!rb) return ; rb.velocity = Vector2.zero; rb.angularVelocity = 0f ; Vector2 dir = (controller.transform.position - transform.position).normalized; dir += Random.insideUnitCircle * 0.6f ; dir.Normalize(); moveSpeed = Random.Range(controller.moveSpeedMin, controller.moveSpeedMax); rotateSpeed = Random.Range(controller.rotateSpeedMin, controller.rotateSpeedMax); rotateDir = Random.value > 0.5f ? 1 : -1 ; rb.velocity = dir * moveSpeed; rb.angularVelocity = rotateSpeed * rotateDir; } private void MaintainMotion () { if (rb.velocity.magnitude < minSpeedThreshold && Time.time - lastResetTime > 0.3f ) { ResetDirection(); } } private void OnCollisionEnter2D (Collision2D collision ) { ResetDirection(); } private void Update () { Move(); } private void OnDrawGizmosSelected () { if (!showDetectRange) return ; Gizmos.color = detectRangeColor; Gizmos.DrawWireSphere(transform.position, detectRadius); } } }
移动相关的接口(这个东西的出现纯粹屎山)
1 2 3 4 5 6 7 8 9 10 using UnityEngine;namespace MyAssets.Framework { public interface IMovable { void Init (BackgroundItemController controller ) ; void Move () ; } }
然后就是背景音乐的管理
然后就是在一个空对象上方放入audio source音源组件,把背景音乐ogg或者wav文件放入,勾选loop循环播放
然后就是我还做了一个更屎山的东西用来管理界面上所有的按钮来确保界面上只能按一个按钮,当按下这个按钮时其他按钮自动禁用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 using System.Collections;using UnityEngine;using UnityEngine.SceneManagement;using UnityEngine.UI;namespace MyAssets.Framework { public class ButtonManager : MonoBehaviour { public static ButtonManager Instance; [Header("按钮组" ) ] public Button[] buttons; private void Awake () { if (Instance == null ) { Instance = this ; } else Destroy(gameObject); if (buttons == null || buttons.Length == 0 ) { Debug.LogWarning("ButtonManager: 未绑定任何按钮!" ); } } public void DisableOther (Button clickedButton ) { if (buttons == null || buttons.Length == 0 ) { return ; } foreach (Button btn in buttons) { if (btn == null ) { continue ; } if (btn != clickedButton) { btn.interactable = false ; } } } public void LoadScene (string sceneName, float delay = 0f ) { StartCoroutine(LoadSceneCoroutine(sceneName, delay)); } private IEnumerator LoadSceneCoroutine (string sceneName, float delay ) { if (delay > 0f ) { yield return new WaitForSeconds (delay ) ; } if (!string .IsNullOrEmpty(sceneName)) { SceneManager.LoadSceneAsync(sceneName); } else { Debug.LogWarning("ButtonManager: 未设置目标场景名!" ); } } } }
然后为了让文字飘动好看的效果,然后又在canvas文字组件里面添加了一个通用脚本控制文字浮动的振幅频率波长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 using TMPro;using UnityEngine;namespace MyAssets.Framework { public class FloatingFont : MonoBehaviour { [Header("振幅" ) ] public float amplitude = 5f ; [Header("频率" ) ] public float frequency = 2f ; [Header("波长" ) ] public float waveLength = 0.5f ; [Header("随机字母相位" ) ] public bool randomPhase = true ; private TMP_Text textMesh; private TMP_TextInfo textInfo; private Vector3[][] originalVertices; private float [] phaseOffsets; private void Awake () { textMesh = GetComponent<TMP_Text>(); } private void Start () { textMesh.ForceMeshUpdate(); textInfo = textMesh.textInfo; int charCount = textInfo.characterCount; originalVertices = new Vector3[charCount][]; phaseOffsets = new float [charCount]; for (int i = 0 ; i < charCount; i++) { if (!textInfo.characterInfo[i].isVisible) continue ; int matIndex = textInfo.characterInfo[i].materialReferenceIndex; int vertIndex = textInfo.characterInfo[i].vertexIndex; var verts = textInfo.meshInfo[matIndex].vertices; originalVertices[i] = new Vector3[4 ]; for (int j = 0 ; j < 4 ; j++) { originalVertices[i][j] = verts[vertIndex + j]; } phaseOffsets[i] = randomPhase ? Random.Range(0f , Mathf.PI * 2f ) : i * waveLength; } } private void Update () { textMesh.ForceMeshUpdate(); textInfo = textMesh.textInfo; for (int i = 0 ; i < textInfo.characterCount; i++) { if (!textInfo.characterInfo[i].isVisible) continue ; int matIndex = textInfo.characterInfo[i].materialReferenceIndex; int vertIndex = textInfo.characterInfo[i].vertexIndex; var verts = textInfo.meshInfo[matIndex].vertices; float wave = Mathf.Sin(Time.time * frequency + phaseOffsets[i]) * amplitude; for (int j = 0 ; j < 4 ; j++) { Vector3 orig = originalVertices[i][j]; verts[vertIndex + j] = orig + new Vector3(0f , wave, 0f ); } } for (int i = 0 ; i < textInfo.meshInfo.Length; i++) { var meshInfo = textInfo.meshInfo[i]; meshInfo.mesh.vertices = meshInfo.vertices; textMesh.UpdateGeometry(meshInfo.mesh, i); } } } }
哇,这些屎山真的不想多看两眼,幸亏这些只是在TVgame场景(TVgame就是游戏场景里的一个游戏机)里的,而不是游戏主世界的,所以后续实在不行就废弃前面所有的操作,重新搞一个吧,这些代码虽然我很清楚脚本之间的联系,但是毋庸置疑这就是屎山,或者这一部分的代码就不要改了,先把别的部分做出来再说别的
所以接下来我会把精力放到游戏玩法上,而不是这个破UI,UI真的讨厌,麻烦至极
下一阶段就是学习新的技巧,以上代码全部搁置不管了
评论区