目录
😪炮台系统
🎶炮口方向跟随鼠标
🎶切换炮台
😪战斗系统
🎮概述
🎮单例模式
🎮开炮
🎮子弹脚本
🎮渔网脚本
🎮鱼属性信息的脚本
😪奖励和等级的实现
😪Unity数据持久化
📖保存游戏
📖继续游戏
📖开始新游戏
最近又跟着教程从头到尾完成了一个实例,虽然是好久之前的教程,但是老师讲的很好,我作为Unity初学者也收获很多,复盘后把收获写在这篇博客上,希望看到的你同样会有收获。
来看一下实例效果:切换炮台、捕鱼、加减金币(消耗炮弹的金币)、等级头衔设置、保存、继续游戏;作为一个捕鱼游戏基本功能都很齐全。
Unity中级案例 - 捕鱼达人哔哩哔哩_bilibili 教程分为上、中、下三部分。上主要对游戏UI进行设计和制作;中负责完成游戏主要玩法战斗系统;下对游戏进一步优化包括特效、音效、游戏保存、游戏发布。
UI和特效这里就先不说了,可以跟着教程实操一下。这里想记录核心玩法的实现和编码思路 ,学会思路和实现方式在其他想写的游戏上也能用到。
炮台系统
炮口方向跟随鼠标
所谓炮口方向跟随鼠标就是给炮口一个旋转朝向鼠标的方向。
我们要做的是获取 鼠标到炮台中心的连线 和 炮口所指位置的 夹角,然后将炮台 z轴旋转的数值 设置到和夹角一致就可以实现炮口方向跟随鼠标转动的目的。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//让炮口跟随鼠标
public class GunFollow : MonoBehaviour
{
public RectTransform UGUICanvas;
public Camera mainCamera;
void Update()
{
//定义鼠标的当前位置
Vector3 mousePos;
//将屏幕坐标转换为世界坐标,结合Input.mousePosition获取鼠标在 当前Canvas(炮台所在的Canvas) 下的位置
RectTransformUtility.ScreenPointToWorldPointInRectangle(UGUICanvas,
new Vector2(Input.mousePosition.x, Input.mousePosition.y), mainCamera,out mousePos);
//定义 z轴 的旋转值
float z;
//判断鼠标在炮口左边还是右边
if (mousePos.x > transform.position.x)
{
//Vector3.up 表示炮口的正方向
//mousePos - transform.position 向量相减得到一条线; 与炮口正方向形成夹角
//Vector3.Angle() 鼠标和炮台的夹角计算方法 返回值为正数 所以要判断鼠标在炮口左边还是右边
z = -Vector3.Angle(Vector3.up, mousePos - transform.position);
}
else
{
z = Vector3.Angle(Vector3.up, mousePos - transform.position);
}
//设置炮口旋转
transform.localRotation = Quaternion.Euler(0, 0, z);
}
}
给所有炮台都挂载上跟随脚本:
切换炮台
炮台在游戏中一共有五种,同样也对应着五种子弹类型(每种子弹类型又有不同颜色与等级相对应),(每一炮所需要的金币数量)类型却有20种。也就是说每一种炮台分别有四挡所消耗的金币数。
来看看脚本是怎么实现的:
public class GameController : MonoBehaviour
{
//所有炮台的预设体,用来更换炮台
public GameObject[] gunGos;
//炮弹生成点父物体
public Transform bulletHolder;
//五种炮弹预制体,对应五种炮台
public GameObject[] bullet1Gos;
public GameObject[] bullet2Gos;
public GameObject[] bullet3Gos;
public GameObject[] bullet4Gos;
public GameObject[] bullet5Gos;
//每一炮所消耗的金币数档位
private int[] oneShootCosts = { 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 };
//使用的是第几档消耗金币数档位
private int costIndex = 0; //五种炮台对应20个消耗金币档位,也就是说每种炮台对应四个消耗金币档位
void Update()
{
ChangeBulletCost();
}
//通过滚轮改变每发炮弹所消耗的钱
void ChangeBulletCost()
{
if (Input.GetAxis("Mouse ScrollWheel") < 0)
{
OnButtonMDown();
}
if (Input.GetAxis("Mouse ScrollWheel") > 0)
{
OnButtonPDown();
}
}
public void OnButtonPDown()
{
//costIndex 表示消耗金币档位,炮台和消耗金币档位是1对4的关系,那么 costIndex / 4 代表了我们在用哪个炮台
//禁用武器
gunGos[costIndex / 4].SetActive(false);
costIndex++;
//播放音效
AudioManager.Instance.PlayEffectSound(AudioManager.Instance.changeClip);
//播放换枪特效
Instantiate(changeEffect);
//防止数组越界
costIndex = (costIndex > oneShootCosts.Length - 1) ? 0 : costIndex;
//启用武器
gunGos[costIndex / 4].SetActive(true);
//更新每发炮弹所消耗的钱
oneShootCostText.text = "$" + oneShootCosts[costIndex];
}
public void OnButtonMDown()
{
//禁用武器
gunGos[costIndex / 4].SetActive(false);
costIndex--;
AudioManager.Instance.PlayEffectSound(AudioManager.Instance.changeClip);
//播放换枪特效
Instantiate(changeEffect);
//防止数组越界
costIndex = (costIndex < 0) ? oneShootCosts.Length - 1 : costIndex;
//启用武器
gunGos[costIndex / 4].SetActive(true);
//更新每发炮弹所消耗的钱
oneShootCostText.text = "$" + oneShootCosts[costIndex];
}
}
战斗系统
概述
当炮弹射出去之后碰到鱼;炮弹会消失生成渔网;渔网生成后也会在特定的时间消失。
同时渔网在消失前会对鱼进行碰撞检测;撞到渔网的鱼也会对自身生命值进行判断;
鱼死了(┏┛<・)))><< 墓┗┓) —— 播放死亡动画销毁自身并生成金币,对应玩家金币数量增加;鱼没死 —— 接着游,炮台接着开炮。
单例模式
我们来看一下实例中单例模式的应用:
public class GameController : MonoBehaviour
{
//单例模式
private static GameController _instance;
public static GameController Instance
{
get
{
return _instance;
}
}
//在Awake()生命周期函数中给 _instance 赋值
void Awake()
{
_instance = this;
}
//定义初始金币和经验值
public int exp = 0;
public int gold = 500;
}
//=================================================================
//当鱼死亡后通过单例模式增加 GameController 脚本中的金币和经验值
//=================================================================
//当鱼死亡加金币和经验值
GameController.Instance.gold += gold;
GameController.Instance.exp += exp;
开炮
每个炮台都有一个空对象用来标记子弹发射点;来看看脚本是怎么实现开炮功能的:
//开炮
void Fire()
{
//当前炮台用的什么子弹,默认是第五种炮使用的炮弹
GameObject[] useBullets = bullet5Gos;
int bulletIndex;
//当按下发射键并没有碰到其他UI按键(比如加减钱按钮、设置按钮)时才会发射炮弹
if (Input.GetMouseButtonDown(0) && EventSystem.current.IsPointerOverGameObject() == false)
{
//判断当前金币够不够开炮
if (gold - oneShootCosts[costIndex] >= 0)
{
switch (costIndex / 4)
{
case 0: useBullets = bullet1Gos; break;
case 1: useBullets = bullet2Gos; break;
case 2: useBullets = bullet3Gos; break;
case 3: useBullets = bullet4Gos; break;
case 4: useBullets = bullet5Gos; break;
}
//根据等级分配哪一套子弹里的哪一种颜色
bulletIndex = (lv % 10 >= 9) ? 9 : lv % 10;
//扣钱
gold -= oneShootCosts[costIndex];
//播放音效
AudioManager.Instance.PlayEffectSound(AudioManager.Instance.fireClip);
//播放开火特效
Instantiate(fireEffect);
//实例化子弹
GameObject bullet = Instantiate(useBullets[bulletIndex]);
bullet.transform.SetParent(bulletHolder, false);
//让子弹的朝向和旋转都和炮口开炮位置一样
bullet.transform.position = gunGos[costIndex / 4].transform.Find("FirePos").transform.position;
bullet.transform.rotation = gunGos[costIndex / 4].transform.Find("FirePos").transform.rotation;
//设置子弹的伤害值
bullet.GetComponent<BulletAttr>().damage = oneShootCosts[costIndex]; //把价格的序号传过去
//让子弹移动
bullet.AddComponent<Ef_AutoMove>().dir = Vector3.up; //改变方向
bullet.GetComponent<Ef_AutoMove>().speed = bullet.GetComponent<BulletAttr>().speed ; //给子弹速度
}
else
{
//TODO Flash The Text;如果金币不足,通过闪烁金币数值提醒玩家
StartCoroutine(GoldNotEnough()); //以协程的方式开启
}
}
}
//金币不足开炮时进行闪烁提示
IEnumerator GoldNotEnough()
{
goldText.color = goldColor;
goldText.color = Color.red;
//让程序在这等待0.5秒然后从这接着运行
yield return new WaitForSeconds(0.5f);
goldText.color = goldColor;
}
子弹脚本
public class BulletAttr : MonoBehaviour
{
//子弹速度
public int speed;
//子弹伤害值
public int damage;
//子弹碰到鱼后生成网的预制体
public GameObject webPrefab;
private void OnTriggerEnter2D(Collider2D collision)
{
//如果撞到边界(屏幕之外设定了一个UI边界),直接销毁自身
if (collision.tag == "Border")
{
Destroy(gameObject);
}
//如果撞到鱼,销毁自身并生成网
if (collision.tag == "Fish")
{
//生成网
GameObject web = Instantiate(webPrefab);
web.transform.SetParent(gameObject.transform.parent, false);
//网的位置等于当前子弹的位置
web.transform.position = gameObject.transform.position;
//将子弹的伤害赋值给网的伤害
web.GetComponent<WebAttr>().damage = damage;
Destroy(gameObject);
}
}
}
渔网脚本
public class WebAttr : MonoBehaviour
{
//网消失的时间
public float disapperTime;
//子弹的伤害
public int damage;
void Start()
{
//隔多长时间销毁网自身
Destroy(gameObject, disapperTime);
}
//检测是否碰到鱼
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag=="Fish")
{
//发送消息 给 FishAttr脚本 的 TakeDamage() 方法
//通知鱼 我(渔网)打到你了 伤害值为 damage
collision.SendMessage("TakeDamage", damage);
}
}
}
鱼属性信息的脚本
//记录鱼的属性信息
public class FishAttr : MonoBehaviour
{
//每种鱼的这些属性值都不一样,自己来设定
public int hp; //血量
public int exp; //经验值
public int gold; //金币值
public int maxNum; //生成最大数量
public int maxSpeed; //最大速度
//鱼死亡预设体
public GameObject diePrefab;
//金币预设体
public GameObject goldPrefab;
private void OnTriggerEnter2D(Collider2D collision)
{
//撞到边界(屏幕之外设定了一个UI边界)就销毁鱼
if (collision.tag == "Border")
{
Destroy(gameObject);
}
}
//鱼受伤
void TakeDamage(int value) //value是从WebAttr脚本传过来的伤害值
{
hp -= value;
//鱼死亡
if (hp <= 0)
{
//当鱼死亡加金币和经验值 —— 单例模式的应用
GameController.Instance.gold += gold;
GameController.Instance.exp += exp;
//播放鱼死亡动画
GameObject die = Instantiate(diePrefab);
die.transform.SetParent(gameObject.transform.parent, false);
die.transform.position = transform.position;
die.transform.rotation = transform.rotation;
//鱼死亡实例化金币
GameObject goldGo = Instantiate(goldPrefab);
goldGo.transform.SetParent(gameObject.transform.parent, false);
goldGo.transform.position = transform.position;
goldGo.transform.rotation = transform.rotation;
//判断鱼身上有没有播放特效的脚本 Ef_PlayEffect
if (gameObject.GetComponent<Ef_PlayEffect>() != null)
{
AudioManager.Instance.PlayEffectSound(AudioManager.Instance.rewardClip);
gameObject.GetComponent<Ef_PlayEffect>().PlayEffect();
}
Destroy(gameObject);
}
}
}
奖励和等级的实现
游戏中为避免玩家金币耗光有两种金币奖励功能:小奖励通过每60s倒计时自动给玩家50金币;大奖励240s倒计时结束后变为按钮,玩家点击按钮会获得500金币然后会重新倒计时。
游戏设定等级称号为:"新手", "入门", "钢铁", "青铜", "白银", "黄金", "白金", "钻石", "大师", "宗师"。根据等级来对应称号
提示玩家升到多少级的UI文本:
脚本实现:
public class GameController : MonoBehaviour
{
//等级
public int lv = 0;
//经验
public int exp = 0;
//初始金币值
public int gold = 500;
public const int bigCountdown = 240; //240秒大奖励
public const int smallCountdown = 60; //60秒小奖励
public float bigTimer = bigCountdown; //计时器
public float smallTimer = smallCountdown;
private string[] lvName = { "新手", "入门", "钢铁", "青铜", "白银", "黄金", "白金", "钻石", "大师", "宗师" };
void Update()
{
//更新等级
UpdateUI();
}
void UpdateUI()
{
bigTimer -= Time.deltaTime; //倒计时
smallTimer -= Time.deltaTime; //倒计时
//如果小计时器小于0,给玩家发金币
if (smallTimer <= 0)
{
//计时器重新计时
smallTimer = smallCountdown;
gold += 50;
}
//大计时器小于0 并且 当按钮没有显示出来才会执行
if (bigTimer <= 0 && bigCountdownButton.gameObject.activeSelf == false)
{
//倒计时结束隐藏计时器
bigCountdownText.gameObject.SetActive(false);
//显示领取金币按钮
bigCountdownButton.gameObject.SetActive(true);
}
//经验等级换算公式:升级所需经验=1000+200*当前等级
while (exp >= 1000 + 20*lv)
{
exp = exp - (1000 + 200 * lv);
lv++;
//提示玩家升到多少级
lvUpTips.SetActive(true);
lvUpTips.transform.Find("Text").GetComponent<Text>().text = lv.ToString();
//启动协程把提示关闭
StartCoroutine(lvUpTips.GetComponent<Ef_HideSelf>().HideSelf(0.6f));
//播放音效
AudioManager.Instance.PlayEffectSound(AudioManager.Instance.lvUpClip);
//播放升级特效
Instantiate(lvEffect);
}
goldText.text = "$" + gold;
lvText.text = lv.ToString();
//如果玩家等级超过99级,就一直是宗师
if ((lv / 10) <= 9)
{
lvNameText.text = lvName[lv / 10];
}
else
{
lvNameText.text = lvName[9];
}
//小计时器 拿到十位数 拿到个位
smallCountdownText.text = " " + (int)smallTimer / 10 + " " + (int)smallTimer % 10;
//大计时器
bigCountdownText.text = (int)bigTimer + "s";
//滑动条显示的是比例 —— 滑动条用来显示等级进度
expSlider.value = ((float)exp) / (1000 + 20 * lv);
}
//大计时器点击领取金币按钮
public void OnBigCountdownButtonDown()
{
gold += 500;
AudioManagerFwl.Instance.PlayEffectSound(AudioManagerFwl.Instance.rewardClip);
//显示加金币特效
Instantiate(goldEffect);
//领取完隐藏按钮
bigCountdownButton.gameObject.SetActive(false);
//显示倒计时
bigCountdownText.gameObject.SetActive(true);
//给倒计时重新赋值
bigTimer = bigCountdown;
}
}
Unity数据持久化
Unity的这种数据持久化是通过 键值对 的方式存储在 Windows注册表 中
unity PlayerPrefs数据存储位置_playerprefer保存的数据位置-CSDN博客文章浏览阅读1.4k次,点赞4次,收藏8次。1、首先查看自己当前工程的名字:可以在Unity->Edit->Project Settings->Player中设置与查看,如图所示:2、按下键盘Win+R键,输入regedit,打开注册表编辑器,找到相应位置查看如下图:_playerprefer保存的数据位置https://blog.csdn.net/Monkey_Xuan/article/details/115518561unity3d--PlayerPrefs 游戏存档_unity 修改playerprefs.setfloat-CSDN博客文章浏览阅读5.7k次,点赞4次,收藏42次。Unity3D游戏开发之数据持久化PlayerPrefs的使用 转载自 本文作者:秦元培,本文出处:http://blog.csdn.net/qinyuanpei/article/details/24195977 博主今天研究了在Unity3D中的数据持久化问题。数据持久化在任何一个开发领域都是一个值得关注的问题,小到一个_unity 修改playerprefs.setfloathttps://blog.csdn.net/acmer_sly/article/details/52675954
保存游戏
游戏一共两个场景:一个Start游戏开始界面场景和Main主要玩法场景。在Main中有一个 返回 按钮点击会返回到开始界面场景中,当我们按下这个按钮保存当前游戏的功能就发挥了作用。
我们需要保存的数据有金币数量、当前等级、当前倒计时数值、还有背景音乐的开关。
当我们按下返回按钮后 :
//返回按钮
public void OnBackButtonDown()
{
//TODO 保存当前游戏 PlayerPrefs只支持 int float string 三种数据类型
//保存金币值
PlayerPrefs.SetInt("gold", GameController.Instance.gold);
//保存等级
PlayerPrefs.SetInt("lv", GameController.Instance.lv);
//小计时器
PlayerPrefs.SetFloat("scd", GameController.Instance.smallTimer);
//大计时器
PlayerPrefs.SetFloat("bcd", GameController.Instance.bigTimer);
//保存经验值
PlayerPrefs.SetInt("exp", GameController.Instance.exp);
int temp = (AudioManager.Instance.IsMute == false) ? 0 : 1;
//保存当前游戏场景的背景音乐是开还是关
PlayerPrefs.SetInt("mute", temp);
//跳转到开始界面场景
UnityEngine.SceneManagement.SceneManager.LoadScene(2);
}
继续游戏
当我们点击完返回按钮来到游戏开始界面后,点击 继续游戏 按钮会获取保存好的玩家信息。
public class GameController : MonoBehaviour
{
//等级
public int lv = 0;
//经验
public int exp = 0;
public int gold = 500;
public const int bigCountdown = 240; //240大奖励
public const int smallCountdown = 60; //60秒小奖励
public float bigTimer = bigCountdown; //计时器
public float smallTimer = smallCountdown;
//读取游戏
void Start()
{
//通过键值对的形式获取保存好的数值
gold = PlayerPrefs.GetInt("gold", gold);
lv = PlayerPrefs.GetInt("lv", lv);
exp = PlayerPrefs.GetInt("exp", exp);
smallTimer = PlayerPrefs.GetFloat("scd", smallCountdown);
bigTimer = PlayerPrefs.GetFloat("bcd", bigCountdown);
UpdateUI();
}
}
开始新游戏
保存好玩家信息后如果点击 开始游戏 ,通过脚本会删除保存好的玩家信息开始新游戏。
using UnityEngine;
using UnityEngine.SceneManagement;
public class StartSceneUI : MonoBehaviour
{
//开始游戏按钮清空数据后在加载游戏场景
public void NewGame()
{
//根据键删除保存好的值
PlayerPrefs.DeleteKey("gold");
PlayerPrefs.DeleteKey("lv");
PlayerPrefs.DeleteKey("exp");
PlayerPrefs.DeleteKey("scd");
PlayerPrefs.DeleteKey("bcd");
SceneManager.LoadScene(1);
}
//继续游戏直接加载游戏场景,在 GameController 脚本获取保存好的玩家信息
public void OldGame()
{
SceneManager.LoadScene(1);
}
public void OnCloseButton()
{
//退出游戏
Application.Quit();
}
}
本篇到这里就结束了,希望我们都能有所收获,拜拜┏(^0^)┛