23.2.4 unity学习 Ruby Advanture项目
//MonoBehaviour类特性说明
void Awake(){}在游戏运行前就已经执行,所以适合初始化一些定义,如获得刚体组件。
void Start(){}在游戏运行时,在最开始执行,所以适合对public输入的值进行初始化操作。
void Update(){}根据不同电脑的性能而不同,反映在Statics里为刷新频率。
void FixedUpdate(){}由于要在不同的设备上实现同样的效果,因此通过固定刷新率来达到对数据更新频率相同。
//Rigidbody2d带有函数性质说明
private OnCollisionEnter2D(Collision2D other){}刚体物体特性,在两个刚体碰撞时触发,Collision2D other为另一个碰撞刚体的特性,用以抓取信息。一般还会用——
另一物体的类 name_you_want = other.gameObject.GetComponent<另一物体的类>();
来获得另一物体的类的其中public的方法。
private OnCollisionExit2D(Collision2D other){}在另一刚体碰撞,并且离开后才触发。
private OnCollisionStay2D(Collision2D other){}在另一刚体碰撞,并且持续碰撞时触发(?)。
private OnTriggerEnter2D(Collision2D other){}与collision不同的是,此函数在本类为trigger时,作为触发器触发。条件与collision相同,下面两个函数也是。
private OnTriggerExit2D(Collision2D other){}
private OnTriggerStay2D(Collision2D other){}
//ruby控制器源码 RubyController.cs
//控制器移动
首先声明public speed,可用来定义移动速度。
因为考虑碰撞,所以加入了rigidbody 2d组件以及box collider 2d(在unity中)。
horizontal, vertical分别存放用户在水平、垂直方向的输入,并且在update()中进行取值。
然后在FixedUpdate()中通过transform.positon直接获取当前物体的位置,通过更改向量的值得出新的位置数值,随后通过rigidbody2d.position更改位置。
Attention:这里不直接更改transform.position,而是交给rigidbody2d去计算碰撞,是因为直接更改位置会出现碰撞物体时不断修正从而产生抖动的问题。
//生命数值(内部数据),更改生命值方法
public maxHealth = 5
private currentHealth
public声明的数据可以直接在unity界面中视情况进行更改,此时赋值只是给定一个default值。private声明的数值则仅可在本类中使用(C#性质,也可以用函数去间接更改)。
更改生命时由于受伤时需要添加无敌时间、受伤动画,所以首先进行判断,此次更改数值amount是否为负数,若成立,判断是否处于无敌状态。若处于无敌状态,则跳过添加无敌时间、受伤动画的判断,若不处于,则使玩家获得无敌时间,同时animator.SetTrigger("Hit"),即播放受伤动画。
最后才为使用Mathf.Clamp,更改currentHealth为(0,maxHealth)之间的数。
//无敌时间计时器
//设置无敌时间为2秒
public float timeinvincible = 2.0f;
//判断是否在无敌期间
bool isInvincible;
//定义变量,设定无敌时间计时器
float invincibleTimer;
由于无敌是个有限时间的buff,且用于多种判断,所以创造最大无敌时间、无敌布尔值,以及用来倒计时的计时器。
若处于无敌状态,在Update()中进行计时器的递减,一旦剩余的计数器时间小于0,则改变布尔值判断为否。
若不处于无敌状态,且受到攻击(表现形式为血量减少,此时判断语句放在扣血的过程中),将计时器值重置为最大无敌时间,改变布尔值判断为正
Attention:刚刚想到的可能出现的错误。对于无敌的判断过于简单,而且timer在update()过程中不断减少,若作为float在过长的运行时间内将timer减得太多可能会导致负数变为正数,此时会迎来非常长一段时间的无敌。
if(isInvincible)
{
invincibleTimer -= Time.deltaTime;
if(invincibleTimer < 0)
{
isInvincible = false;
}
}
因此最好加上判断,将timer的负数范围控制在一个区间内,如:
if(isInvincible)
{
invincibleTimer -= Time.deltaTime;
if(invincibleTimer < 0)
{
isInvincible = false;
}//将invincibleTimer的范围始终控制在-128到-5之间,使得不会出现负数减多了成正
if(invincibleTimer < -128.0f) invincibleTimer = -5.0f
}
//动画组件
//声明动画管理组件
Animator animator;
//设定一个二维矢量,决定ruby在站立不动时的朝向
Vector2 lookdirection = new Vector2(1, 0);
Vector2 move;
将之前获取用户输入的horizontal, vertical拿来创建新的矢量 Vector2(horizontal, vertical),判断用户有输入的情况是move.x, move.y都不等于0,则lookdirection表示了角色看的方向。
之后的所有行为,如果有对应的动画动作,则animator.SetTrigger()
//子弹发射
//声明游戏对象,使得ruby能够挂接projectile的prefab
public GameObject projectilePrefab;。
。
。
void RubyLaunch()
{
//在指定位置创建游戏对象,即projectPrefeb在刚体位置上方0.5个单位被创建出来
GameObject projectileObject = Instantiate(projectilePrefab,rigidbody2d.position+Vector2.up*0.5f,Quaternion.identity);
//获取刚刚生成的子弹游戏对象
ProjectileControl projectile = projectileObject.GetComponent<ProjectileControl>();
//通过脚本移动子弹位置
projectile.Launch(lookdirection, 300);animator.SetTrigger("Launch");
}
通过声明public GameObject,挂载别的prefab文件,用以获取游戏对象。并且可以调用类中方法。
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class RubyController : MonoBehaviour
{
//使速度可以在unity里面直接更改
public float speed = 0.1f;
//声明刚体对象
Rigidbody2D rigidbody2d;
//获取用户输入
float horizontal;
float vertical;
//设置生命上限
public int maxHealth = 5;
private int currentHealth;
public int getHealth() { return currentHealth; }
//设置无敌时间为2秒
public float timeinvincible = 2.0f;
//判断是否在无敌期间
bool isInvincible;
//定义变量,设定无敌时间计时器
float invincibleTimer;
//声明动画管理组件
Animator animator;
//设定一个二维矢量,决定ruby在站立不动时的朝向
Vector2 lookdirection = new Vector2(1, 0);
Vector2 move;
//声明游戏对象,使得ruby能够挂接projectile的prefab
public GameObject projectilePrefab;
//------------------------------- Start is called before the first frame update
void Start()
{
//QualitySettings.vSyncCount= 0;//垂直同步变为0,使得改帧能成立
//Application.targetFrameRate = 60;//改帧,使得帧数统一
//获取当前游戏对象刚体组件,通过刚体组件移动
rigidbody2d = GetComponent<Rigidbody2D>();
//初始化生命值
currentHealth = maxHealth-2;
//获取动画组件
animator = GetComponent<Animator>();
}
//-------------------------------- Update is called once per frame
void Update()
{
horizontal = Input.GetAxis("Horizontal");
vertical = Input.GetAxis("Vertical");
//Vector2 position = transform.position;
//position.x = position.x + speed * horizontal * Time.deltaTime;// 进阶:使用Time.deltaTime是因为对于不同电脑,帧数能达到的不一样,物理效果不一样。因此用“帧/秒”来用整体时间配平刷新
//position.y = position.y + speed * vertical * Time.deltaTime;//但是加入Time.deltaTime后明显感觉有卡顿,可能是计算太麻烦了。
//transform.position = position;
//判断是否在无敌时间,进行计时器倒计时
if(isInvincible)
{
invincibleTimer -= Time.deltaTime;
if(invincibleTimer < 0)
{
isInvincible = false;
}
}
//创建二维矢量表示ruby移动的数据
move = new Vector2 (horizontal, vertical);
if(!Mathf.Approximately(move.x,0.0f)||!Mathf.Approximately(move.y,0.0f))
{
lookdirection.Set(move.x, move.y);
//表示方向的向量一般都要“归一化”,使得不超过取值范围
lookdirection.Normalize();
}
animator.SetFloat("Look X",lookdirection.x);
animator.SetFloat("Look Y",lookdirection.y);
animator.SetFloat("Speed", move.magnitude);
//添加发射子弹的逻辑,获取用户输入J,或者project settings里面的fire1,也就是鼠标左键
if (Input.GetKeyDown(KeyCode.J)||Input.GetAxis("Fire1")!=0)
{
RubyLaunch();
}
}
//--------------------------------------固定时间间隔的刷新方法
private void FixedUpdate()
{
//将update里面的代码拿过来,为的是用固定刷新频率计算位置。可优化碰撞时导致的不断回弹,所以用rigidbody.position来改,而不直接更改transform。
//优化碰撞时转向使用的是Rigidbody2D中的freeze rotation [z]。
Vector2 position = transform.position;
position.x = position.x + speed * horizontal * Time.deltaTime;
position.y = position.y + speed * vertical * Time.deltaTime;
rigidbody2d.position = position;
}
//更改生命值方法
public void ChangeHealth(int amount)
{
if (amount < 0)
{
if (isInvincible)
{
return;
}
//受伤,此时令玩家获得无敌
isInvincible = true;
//重置无敌状态计时器
invincibleTimer = timeinvincible;
//播放受伤动画
animator.SetTrigger("Hit");
}
//Math.Clamp用来限制生命值的范围,不能小于0,不能大于生命值上限
currentHealth = Mathf.Clamp(currentHealth + amount,0, maxHealth);
Debug.Log("当前生命值: " + currentHealth + "/" + maxHealth);
}
//玩家发射子弹
void RubyLaunch()
{
//在指定位置创建游戏对象,即projectPrefeb在刚体位置上方0.5个单位被创建出来
GameObject projectileObject = Instantiate(projectilePrefab,rigidbody2d.position+Vector2.up*0.5f,Quaternion.identity);
//获取刚刚生成的子弹游戏对象
ProjectileControl projectile = projectileObject.GetComponent<ProjectileControl>();
//通过脚本移动子弹位置
projectile.Launch(lookdirection, 300);
animator.SetTrigger("Launch");
}
}
//敌人行为控制 EnemyController2.cs
//行为控制
此敌人AI简易,只拥有水平与垂直两种行动方向以及朝一个方向的运动时间。变量direction用来存储1or-1,分别表示正向与逆向。
Attention:此时对于敌人动画的方向控制也可以使用direcition来表示,但是对于position的计算方式就要使用|speed| * Time.deltaTime * direction。因为speed与direction同号,所以不加绝对值的话就会导致方向恒正。
//检测碰撞
与别的OnCollisionEnter2D()无特别之处,只是需要注意,由于可能会出现碰撞没有检测到物体的情况,所以最好加上此检测
RubyController rubyController = other.gameObject.GetComponent<RubyController>(){
if (rubyController != null){
...
}
}
从而避免在未检测到物体类的情况下调用不知道的类方法,引发错误。
//在敌人被击败后,取消物理引擎
rigidbody2d.simulated = false;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyController2 : MonoBehaviour
{
public float speed = 1.0f;
public bool isvertical;
Rigidbody2D rigidbody2d;
//朝一个方向运动的总时间
public float changeTime = 2.0f;
//计时器
float timer;
//方向,正向为1,反向为-1
int direction = 1;
//声明动画管理者组件
Animator animator;
//判断机器人是否坏了
bool broked = true;
// Start is called before the first frame update
void Start()
{
rigidbody2d = GetComponent<Rigidbody2D>();
timer = changeTime;
animator = GetComponent<Animator>();
//在开始时对direction进行一次判断,使direction的正负号与speed一致,防止出现动画放反的问题!!!
//非常重要,根据情况可能得将以下代码放入update或者fixedupdate以不停更新,但目前还没有这方面需求
if(speed>=0) { direction = 1; }
else { direction = -1; }
}
// Update is called once per frame
//在update中操作重置计时器
void Update()
{
//判断机器人是否为坏
if(!broked)
{
return;
}
timer -= Time.deltaTime;
if (timer < 0)
{
direction = -direction;
timer = changeTime;
}
}
void FixedUpdate()
{
//同上
if (!broked)
{
return;
}
Vector2 position = rigidbody2d.position;
//Debug.Log($"此时的direction值为:{direction}");
if (isvertical)
{
position.y += Mathf.Abs(speed) * Time.deltaTime * direction;
//设置animator中的参数
animator.SetFloat("MoveX", 0);
animator.SetFloat("MoveY", direction);
}
else
{
position.x += Mathf.Abs(speed) * Time.deltaTime * direction;
animator.SetFloat("MoveX", direction);
animator.SetFloat("MoveY", 0);
}
rigidbody2d.MovePosition(position);
}
//刚体碰撞事件,而非触发器事件
private void OnCollisionEnter2D(Collision2D other)
{
//获取玩家角色对象
RubyController rubyController = other.gameObject.GetComponent<RubyController>();
if (rubyController != null)
{
Debug.Log("检测到玩家和怪物的碰撞");
rubyController.ChangeHealth(-1);
}
}
//修复机器人的方法
public void Fix()
{
broked = false;
//让机器人不再会碰撞,取消物理引擎
rigidbody2d.simulated= false;
}
}
//陷阱地段 DamagableZone.cs
//对接触物体进行伤害
最普通的使用接触物体类方法,不过这里使用的是OnTriggerStay()。且在人物血量大于0的时候才生效。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DamagableZone : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
//声明伤害量
public int amount = -1;
private void OnTriggerStay2D(Collider2D other)
{
RubyController rubycontroller = other.GetComponent<RubyController>();
if (rubycontroller != null)
{
if(rubycontroller.getHealth() > 0)
{
rubycontroller.ChangeHealth(amount);
}
}
}
}
//可收集的物品 HealthCollectile.cs
//对收集者进行类操作,并摧毁本物体
与前trigger的碰撞过程类似,不过是在是否收集到的问题上进行了条件判断。若条件达成,则摧毁本物体。
Destroy(gameObject);
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCollectible : MonoBehaviour
{
Start is called before the first frame update
//void Start()
//{
//}
Update is called once per frame
//void Update()
//{
//}
//公开草莓可以恢复的血量
public int amount=1;
//添加触发器碰撞事件,前面的start和update都不用管
private void OnTriggerEnter2D(Collider2D other)
{
Debug.Log("已碰撞");
//获取ruby对象的脚本对象
RubyController rubycontroller = other.GetComponent<RubyController>();
if (rubycontroller != null)//检查有没有出错,出错的话获取到的就是null对象
{
if(rubycontroller.getHealth() < rubycontroller.maxHealth)//判断,如果血量没满才吃当前物体治疗
{
rubycontroller.ChangeHealth(amount);//这里要保证rubycontroller这个类中的changehealth方法是public,才能被访问到
//被collect以后摧毁当前物体
Destroy(gameObject);
}
else
{
Debug.Log("血量已满,无法吃血包");
}
}
else
{
Debug.LogError("Did not get rubycontroller");
}
}
}
//可发射的物体控制器 ProjectileControl.cs
//初始化本物体
void Awake()
{
rigidbody2d= GetComponent<Rigidbody2D>();
}
此处使用Awake()而非Start(),是因为如果用Start,rigidbody自身并未生成,仅仅是被实例化了。而类中的Launch()方法又对rigidbody进行力的施加,从而导致error产生。详细说法见下图
其他方法与HealthCollectible.cs内的内容相类似,不多加赘述。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ProjectileControl : MonoBehaviour
{
Rigidbody2D rigidbody2d;
// Start is called before the first frame update
void Awake()
{
rigidbody2d= GetComponent<Rigidbody2D>();
}
// Update is called once per frame
void Update()
{
//使子弹在飞行过远距离后自动销毁
if(transform.position.magnitude>100.0f)
{
Destroy(gameObject);
}
}
public void Launch(Vector2 direction, float force)
{
rigidbody2d.AddForce(direction * force);
}
private void OnCollisionEnter2D(Collision2D other)
{
//获取子弹碰撞到的机器人对象的脚本组件
EnemyController2 enemyController2 = other.collider.GetComponent<EnemyController2>();
if(enemyController2 != null )
{
enemyController2.Fix();
}
Debug.Log($"齿轮物体碰撞到{other.gameObject}");
Destroy(gameObject);
}
}