目录
- 初始化项目开发环境
- 初始化项目
- 屏幕自适应
- 游戏UI界面元素布局
- 开始界面UI
- 角色选择(商城)界面UI
- 游戏界面UI
- 地图生成算法之菱形布局
- Resources资源加载
- 代码生成地图菱形布局
- 地图生成算法之墙壁边界
- 菱形地图双排布局
- 地图瓷砖颜色美化
- 墙壁边界生成
- 地图生成算法之数据管理
- 美化瓷砖地图
- 地图数据存储
- 地图数据存储测试
- 角色出生与基本移动控制
- 角色出生
- 地图信息展示
- 角色基本移动
- 蜗牛痕迹与移动边界控制
- 蜗牛痕迹
- 移动边界控制
- 地图生成算法之连续生成
- 地图连续生成
- 摄像机跟随角色移动
- 摄像机跟随
- 地图生成算法之地面塌陷
- 摄像机自动跟随
- 地面塌陷
- 地图生成算法之路障生成
- 何为概率
- 路障生成概率
- 使用协程生成路障移动动画
- 路障生成优化
- 路障动画
- 角色与路障交互死亡判断
- 路障蜗牛痕迹
- 死亡判断
- 游戏奖励物品生成与交互
- 奖励物体生成
- 触发吃掉奖励物品
- PlayerPrefs存储游戏分数信息
- PlayerPrefs
- 游戏分数信息存储
- 游戏UI界面逻辑【上】
- UI界面逻辑
- 游戏UI界面逻辑【下】
- Android触屏操作与APK细节配置
- 触屏操作
- APK细节配置
初始化项目开发环境
初始化项目
1、创建项目
添加如下文件夹
2、NGUI导入(示例工程不要导入)
将下载好的NGUI Next-Gen UI拖入Assets,并将如图所示取消
3、相关资源导入
①将所有图片资源拖入UI中
②将模型资源拖入Assets
③将字体文件拖入Fonts
保存当前场景为01
4、Atlas图集资源制作
①打开Atlas Maker
②选中UI中的所有图片资源
③点击Create
屏幕自适应
1、什么是屏幕自适应?
1、随着Android类手机的不断发展,各种各样的Android手机厂商推出了各
种各样的Android手机。随着硬件的不断升级,手机屏幕的尺寸越来越大,屏
幕分辨率也变的千奇百怪。
2、款商业游戏正式上线前都会进行几百台手机测试,其中主要测试的是就是屏幕自适应,以及是否有兼容性的Bug。
3、屏幕自适应:主要是游戏的UI界面在不同尺寸,不同屏幕比例的手机上都能在正常的位置显示。
2、显示分辨率以及比例
本套课程以16:9的分辨率进行自适应处理。
比例值是:1920/1080=1.777
19201080
1280720
960540
720405
640360
320180
3、NGUI初步实现UI自适应
Game面板创建显示用的分辨率尺寸
将如下四种尺寸添加:
19201080
1280720
960540
720405
4、UIRoot组件相关设置:
1>缩放类型:Constrained On Mobiles
2>内容宽度/高度:填写数值,勾选Fit
游戏UI界面元素布局
开始界面UI
游戏开始界面,主要功能用于开始游戏,选择角色,展示积分以及游戏功能设置。
当前界面的根名为:Start_Ul。
创建空物体,并命名Start_UI
添加图片logo,点击Snap,并选择Based On Width,并调整合适大小位置,将该图片更名为logo.
将其他图片如图摆放:
NGUI中添加label标签:
继续利用label标签和Sprite标签做出如下图所示:
角色选择(商城)界面UI
角色选择界面有两个功能:
功能一,展示游戏所有的角色;功能二,使用金币购买角色。
当前界面的根名为:Shop_UI
由于开始界面和角色选择界面有相同之处,可以将Start_UI复制粘贴并更名为Shop_UI,在此基础上进行修改。
如图:
将play图片失活,添加另一种play图片制作如下:
其中文本100可以作为该图片的子对象存在。
游戏界面UI
游戏界面,游戏的主要操作界面,可以暂停游戏,实时显示当前得分。
当前界面的根名为:Game_UI
地图生成算法之菱形布局
Resources资源加载
1.动态加载资源
目的是替代Inspector面板拖拽赋值。
因为拖拽赋值耦合性太高,且如果资源过多时,拖拽赋值很容易出错,而且还不
容易管理。
2.Resources 动态加载资源
1新建资源目录“Resources”存放预制体文件;
2使用Resources.Load(“资源名”)进行动态资源加载;
代码生成地图菱形布局
1.普通矩形平铺生成
底板瓷砖模型的宽度是:0.254f;
两层For循环嵌套生成长方形的地面效果。
Quaternion.Euler(Vector3)可以将三维向量转换为四元数。
①创建Resources文件夹
②将tile_white预设体拖入Resources文件夹
③创建空物体,并命名为:MapManager,并创建相应脚本。
④使用Resources动态资源加载tile_white
⑤查找MapManager对象
private Transform m_Transform;
void Start()
{
m_Transform = gameObject.GetComponent<Transform>();
}
⑥将创建的每一个tile的父对象设置为MapManager
private void CreateMapItem()
{
for(int i = 0;i < 10;i++)
{
for(int j = 0;j < 5;j++)
{
GameObject tile = GameObject.Instantiate(m_prefab_tile,Vector3.zero, Quaternion.identity);
tile.GetComponent<Transform>().SetParent(m_Transform);
}
}
}
⑦设置瓷砖位置
由于底板瓷砖模型的宽度是:0.254f
Vector3 pos = new Vector3(j * 0.254f, 0, i * 0.254f);
⑧设置瓷砖角度
Vector3 rot = new Vector3(-90, 0, 0);
⑨设置主摄像机
size:1.4
Projection:Orthographic
Far:15
并设置到合适位置
2.等腰直角三角形求底边实现单排菱形平铺
等腰直角三角形求底边:2的平方根 乘以 直角边长;
Unity3D代码实现:
M
a
t
h
f
.
S
q
r
t
(
2
)
∗
n
;
Mathf.Sqrt(2)*n;
Mathf.Sqrt(2)∗n;
①旋转角度
Vector3 rot = new Vector3(-90, 45, 0);
②调整位置
Vector3 pos = new Vector3(j * Mathf.Sqrt(2) * 0.254f, 0, i * Mathf.Sqrt(2) * 0.254f);
③调整摄像机
优化:
private float bottomLength = Mathf.Sqrt(2) * 0.254f;
Vector3 pos = new Vector3(j * bottomLength, 0, i * bottomLength);
地图生成算法之墙壁边界
菱形地图双排布局
第二排菱形位置需要偏移,偏移量为半个底边长度。
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 5; j++)
{
Vector3 pos = new Vector3(j * bottomLength + bottomLength / 2, 0, i * bottomLength + bottomLength / 2);
Vector3 rot = new Vector3(-90, 45, 0);
GameObject tile = GameObject.Instantiate(m_prefab_tile, pos, Quaternion.Euler(rot));
tile.GetComponent<Transform>().SetParent(m_Transform);
}
}
地图瓷砖颜色美化
1.颜色数值转换
Unity的Inspector 面板上的颜色R,G,B都是0~255表示;
但是我们在代码中表示颜色的R,G,B使用的是0~1;
但是我们日常工作中通用的是0~255的表示方法,但是为了在代码中使用,就
需要进行数值转换。
转换公式:颜色值/255f= 小数值;
①更改颜色值
找到每一个瓷砖的MeshRenderer组件中的material中的color进行更改
tile.GetComponent<MeshRenderer>().material.color = Color.blue;
tile.GetComponent<Transform>().Find("normal_a2").GetComponent<MeshRenderer>().material.color = Color.blue;
②设置自定义颜色
由于纯色可能会影响美观,我们将颜色设置为自定义颜色
private Color colorOne = new Color(124 / 255f, 155 / 255f, 230 / 255f);
③修改瓷砖的光滑度
由于瓷砖表面看起来会反光,在瓷砖预设体中的Shader中修改Smoothness为0
④同理将双排数瓷砖颜色进行设置
private Color colorTwo = new Color(125 / 255f, 169 / 255f, 233 / 255f);
墙壁边界生成
奇数行的头和尾两块瓷砖使用墙壁模型生成
①将预设体中的wall2拖入Resources
②动态加载wall2资源
m_prefab_wall = Resources.Load("wall2") as GameObject;
③添加瓷砖
对于单数排瓷砖,可以在瓷砖开头和结尾添加墙壁,中间则添加瓷砖
if(j == 0 || j == 4)
{
tile = GameObject.Instantiate(m_prefab_wall, pos, Quaternion.Euler(rot));
}
else
{
tile = GameObject.Instantiate(m_prefab_tile, pos, Quaternion.Euler(rot));
}
若此时运行则会发生空指针引用异常,由于此句代码会寻找瓷砖的子物体,但墙壁预设体没有子物体,所以报错
tile.GetComponent<Transform>().Find("normal_a2").GetComponent<MeshRenderer>().material.color = colorOne;
解决办法:将该代码与生成瓷砖时同时发生。
④调整地图宽度
将代码中j的最大长度加一个。
地图生成算法之数据管理
美化瓷砖地图
1、摄像机拍摄角度和位置调整
2、灯光位置与角度以及颜色调整
3、墙壁颜色调整
private Color colorWall = new Color(87 / 255f, 93 / 255f, 169 / 255f);
地图数据存储
使用List集合加数组的形式,将地图存储起来。
横向存储进数组,纵向存储进List集合。
①引入命名空间
using System.Collections.Concurrent;
②声明数据存储集合
//地图数据存储
private List<GameObject[]> mapList = new List<GameObject[]>();
③修改地图生成代码
使得地图一排一排生成,而不是先生成单数排,再生成双数排
for(int i = 0;i < 10;i++)
{
for(int j = 0;j < 6;j++)
{
Vector3 pos = new Vector3(j * bottomLength, 0, i * bottomLength);
Vector3 rot = new Vector3(-90, 45, 0);
GameObject tile = null;
if(j == 0 || j == 5)
{
tile = GameObject.Instantiate(m_prefab_wall, pos, Quaternion.Euler(rot));
tile.GetComponent<MeshRenderer>().material.color = colorWall;
}
else
{
tile = GameObject.Instantiate(m_prefab_tile, pos, Quaternion.Euler(rot));
tile.GetComponent<Transform>().Find("normal_a2").GetComponent<MeshRenderer>().material.color = colorOne;
tile.GetComponent<MeshRenderer>().material.color = colorOne;
}
tile.GetComponent<Transform>().SetParent(m_Transform);
}
for (int j = 0; j < 5; j++)
{
Vector3 pos = new Vector3(j * bottomLength + bottomLength / 2, 0, i * bottomLength + bottomLength / 2);
Vector3 rot = new Vector3(-90, 45, 0);
GameObject tile = GameObject.Instantiate(m_prefab_tile, pos, Quaternion.Euler(rot));
tile.GetComponent<MeshRenderer>().material.color = colorTwo;
tile.GetComponent<Transform>().Find("normal_a2").GetComponent<MeshRenderer>().material.color = colorTwo;
tile.GetComponent<Transform>().SetParent(m_Transform);
}
}
④数据存储
在每一排地图生成前,声明一个临时数组,用来存储每一排的瓷砖
GameObject[] item = new GameObject[6];
每次瓷砖生成时,将瓷砖加入临时数组
item[j] = tile;
当一排瓷砖生成结束之后,将该临时数组加入MapList中
mapList.Add(item);
地图数据存储测试
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
string str = "";
for (int i = 0; i < mapList.Count; i++)
{
for(int j = 0;j < mapList[i].Length; j++)
{
str += mapList[i][j].name;
}
str += "\n";
}
Debug.Log(str);
}
}
角色出生与基本移动控制
角色出生
读取地图集合中的数据,指定主角模型的出生点和旋转状态
①确定角色模型cube_car
②创建PlayerController脚本,将其拖到该预设体上
③找到该预设体的Transform组件,MapManager脚本
private Transform m_Transform;
private MapManager m_MapManager;
void Start()
{
m_Transform = gameObject.GetComponent<Transform>();
m_MapManager = GameObject.Find("MapManager").GetComponent<MapManager>();
}
为了便于访问MapManager中的MapList,将private修改为public
按下M键角色出生:
void Update()
{
if(Input.GetKeyDown(KeyCode.M))
{
m_Transform.position = m_MapManager.mapList[3][2].GetComponent<Transform>().position + new Vector3(0,0.254f / 2,0);
m_Transform.rotation = m_MapManager.mapList[3][2].GetComponent<Transform>().rotation;
}
}
优化代码:
private int z = 3;
private int x = 2;
Transform PlayerPos = m_MapManager.mapList[z][x].GetComponent<Transform>();
m_Transform.position = PlayerPos.position + new Vector3(0,0.254f / 2,0);
m_Transform.rotation = PlayerPos.rotation;
地图信息展示
在地图模型上显示数据集合的信息
①将瓷砖和墙壁的icon改为绿色
②当按下空格打印地图数据时添加如下代码
mapList[i][j].name = i + "--" + j;
角色基本移动
使用A和D进行按键控制。读取地图中的位置信息,进行角色移动效果模拟。
①将按下M键的逻辑封装为一个方法
void SetPlayerPos()
{
Transform PlayerPos = m_MapManager.mapList[z][x].GetComponent<Transform>();
m_Transform.position = PlayerPos.position + new Vector3(0, 0.254f / 2, 0);
m_Transform.rotation = PlayerPos.rotation;
}
②修改Update()逻辑
void Update()
{
if(Input.GetKeyDown(KeyCode.M))
{
SetPlayerPos();
}
if(Input.GetKeyDown(KeyCode.A))
{
z++;
if(z % 2 == 1)
{
x--;
}
SetPlayerPos();
}
if(Input.GetKeyDown(KeyCode.D))
{
z++;
if(z % 2 == 0)
{
x++;
}
SetPlayerPos();
}
}
蜗牛痕迹与移动边界控制
蜗牛痕迹
角色经过的瓷砖要改变颜色,类似于蜗牛经过的痕迹
①角色控制中声明两种颜色
private Color colorOne = new Color(122 / 255f, 85 / 255f, 179 / 255f);
private Color colorTwo = new Color(126 / 255f, 93 / 255f, 183 / 255f);
②在SetPlayerPos中对经过瓷砖进行渲染
void SetPlayerPos()
{
Transform PlayerPos = m_MapManager.mapList[z][x].GetComponent<Transform>();
MeshRenderer normal_a2 = PlayerPos.Find("normal_a2").GetComponent<MeshRenderer>();
if(z % 2 == 0)
{
normal_a2.material.color = colorOne;
}
else
{
normal_a2.material.color = colorTwo;
}
m_Transform.position = PlayerPos.position + new Vector3(0, 0.254f / 2, 0);
m_Transform.rotation = PlayerPos.rotation;
}
移动边界控制
角色移动到左右边界时,就不可再移动。
角色移动到上方边界时,地图就需要继续生成。
①修改右边界
观察地图上的下标,发现当x = 0时,z不能再加一,且x = 0时,x不能再减一。即当按下A键的控制逻辑修改为:
if(Input.GetKeyDown(KeyCode.A))
{
if(x != 0)
{
z++;
}
if(z % 2 == 1 && x != 0)
{
x--;
}
SetPlayerPos();
}
②修改左边界
当按下D键控制逻辑修改为:
if(Input.GetKeyDown(KeyCode.D))
{
if(x != 4 || z % 2 == 0)
{
z++;
}
if(z % 2 == 0 && x != 4)
{
x++;
}
SetPlayerPos();
}
地图生成算法之连续生成
地图连续生成
当我们的角色快接近地图前方边缘的时候,需要动态的继续生成地图。
①优化代码:
对于向左向右移动逻辑单独封装为一个函数PlayerControl()
private void PlayerControl()
{
if (Input.GetKeyDown(KeyCode.A))
{
if (x != 0)
{
z++;
}
if (z % 2 == 1 && x != 0)
{
x--;
}
SetPlayerPos();
}
if (Input.GetKeyDown(KeyCode.D))
{
if (x != 4 || z % 2 == 0)
{
z++;
}
if (z % 2 == 0 && x != 4)
{
x++;
}
SetPlayerPos();
}
}
②将CreateMapItem()的访问修饰符改为public
③由于每次生成地图都会在0这个位置生成,因此需要加一个偏移值,将CreateMapItem()添加参数Float offsetZ。
④创建计算角色位置函数CalcPosition()
private void CalcPosition()
{
if(m_MapManager.mapList.Count - z <= 5)
{
float offsetZ = m_MapManager.mapList[m_MapManager.mapList.Count - 1][0].GetComponent<Transform>().position.z + m_MapManager.bottomLength / 2;
m_MapManager.CreateMapItem(offsetZ);
}
}
摄像机跟随角色移动
摄像机跟随
在我们的角色移动的时候,摄像机要跟随角色移动。
使用插值运算函数,让摄像机平滑移动:
Vecotr3.Lerp ( Vector3 from, Vector3 to, float t) ;
①创建脚本CameraFollow,并拖到MainCamera
②定义m_Transform并获取组件,即摄像机位置
③定义m_Palyer并获取组件,即玩家位置
m_Player = GameObject.Find("cube_dice").GetComponent<Transform>();
④定义bool型变量,用来控制何时摄像机跟随
⑤定义CameraMove方法
根据主摄像机相对玩家位置找到偏移值,由于摄像机是相对玩家z轴偏移2.08.
由于不希望摄像机左右移动,因此将x轴向位置更新为摄像机x轴向位置
void CameraMove()
{
if(StartFollow)
{
Vector3 nextPos = new Vector3(m_Transfrom.position.x, m_Player.position.y + 2.08f, m_Player.position.z);
m_Transfrom.position = nextPos;
}
}
⑥使用插值运算将摄像机平滑移动
m_Transfrom.position = Vector3.Lerp(m_Transfrom.position, nextPos, Time.deltaTime);
地图生成算法之地面塌陷
摄像机自动跟随
当我们按下M键初始化完毕角色位置后,摄像机就开始自动跟随移动。
①在角色控制脚本中,定义m_CameraFollow,并获取
m_CameraFollow = GameObject.Find("Main Camera").GetComponent<CameraFollow>();
②当按下M键时,将StartFollow初始化为true,此时摄像机将自动跟随
地面塌陷
在我们的角色向前移动的同时,后面的地面要不断的塌陷掉落,增加紧迫感。
1>要让地面一排一排的掉落
对每一排瓷砖添加刚体组件使其掉落。
使用协程固定时间间隔,实现地面塌陷。
①定义协程函数TileDown()
private IEnumerator TileDown()
{
while (true)
{
yield return new WaitForSeconds(0.5f);
for(int i = 0;i < mapList[index].Length;i++)
{
mapList[index][i].AddComponent<Rigidbody>();
}
index++;
}
}
②将开启协程函数封装
public void StartTileDown()
{
StartCoroutine(TileDown());
}
③将关闭协程函数封装
public void StopTileDown()
{
StopCoroutine(TileDown());
}
④当按下M键时调用开始塌陷函数
2>掉落的地面元素要销毁
定时销毁游戏物体。
①添加刚体之后,瓷砖要销毁
GameObject.Destroy(mapList[index][i],1.0f);
3>给掉落的地面元素添加旋转力
rigidbody.angularVelocity:角速度向量。
将添加刚体时的代码修改为:
Rigidbody rb = mapList[index][i].AddComponent<Rigidbody>();
rb.angularVelocity = new Vector3(Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f)) * Random.Range(1,10);
方向和力随机。
4>停止地面塌陷(游戏结束)
当塌陷到的位置和角色的位置重合,即停止塌陷。
①在MapManager中声明并获取m_PlayerControlloer,以便访问玩家坐标
m_PlayerController = GameObject.Find("cube_dice").GetComponent<PlayerController>();
②将PlayerController中z和x成员变量访问修饰符改为public
③如果横坐标相同,则停止塌陷
if(m_PlayerController.z == index)
{
StopTileDown();
}
地图生成算法之路障生成
何为概率
在我们的游戏地图中,墙壁是需要固定生成的。
而瓷砖地面有的地方是需要生成坑洞的,有的地方是需要生成陷阱的,有的地方
是需要生成奖励物品的,这些游戏地图中非固定物体(陷阱,奖励)的生成是有
概率的,也就是说是随机的,这样游戏才更有可玩性。
路障生成概率
游戏地图中的“路障”分三种:
1坑洞 2地面陷阱 3天空陷阱
我们的地图,第一段是不生成“路障”的,从第二段开始,即生成路障。
1>概率算法
定义概率值,比如说概率为5%,我们就使用随机数在0~100之间随机,如果
随机得到的数小于或等于5(0~5之间),就说明满足生成路障的概率。
2>坑洞概率生成:
概率基数:0%
路障每生成一段,基数+2%
取值范围:1~30
①定义坑洞生成概率变量
②定义计算概率函数
/// <summary>
/// 计算概率
/// 0:瓷砖
/// 1:坑洞
/// 2:地面陷阱
/// 3:天空陷阱
/// </summary>
/// <returns></returns>
private int CalcPR()
{
int pr = Random.Range(1, 100);
if(pr <= pr_hole)
{
return 1;
}
return 0;
}
③定义增加概率函数
private void AddPR()
{
pr_hole += 2;
}
④当生成新的一段路面时,增加概率
在PlayerController中的CalcPosition()函数中,满足条件时生成路面。
⑤生成瓷砖时根据概率选择是生成瓷砖还是坑洞
int pr = CalcPR();
if(pr == 0)
{
tile = GameObject.Instantiate(m_prefab_tile, pos, Quaternion.Euler(rot));
tile.GetComponent<Transform>().Find("normal_a2").GetComponent<MeshRenderer>().material.color = colorOne;
tile.GetComponent<MeshRenderer>().material.color = colorOne;
}
else if (pr == 1)
{
tile = new GameObject();
tile.GetComponent<Transform>().position = pos;
tile.GetComponent<Transform>().rotation = Quaternion.Euler(rot);
}
3>地面陷阱概率生成:
概率基数:0%
路障每生成一段,基数+1%
取值范围:31~60
①定义m_prefab_spikes(GameObject),并动态加载
②定义pr_spikes(int)
③修改增加概率函数
public void AddPR()
{
pr_hole += 2;
pr_spikes += 1;
}
④修改计算概率函数
private int CalcPR()
{
int pr = Random.Range(1, 100);
if(pr <= pr_hole)
{
return 1;
}
else if(pr > 31 && pr < pr_spikes + 30 )
{
return 2;
}
return 0;
}
⑤地面生成时,添加判断条件
else if(pr == 2)
{
tile = GameObject.Instantiate(m_prefab_spikes, pos, Quaternion.Euler(rot));
}
4>天空陷阱概率生成:
概率基数:0%
路障每生成一段,基数+1%
取值范围:61~90
①定义pr_sky_spikes
private int pr_sky_spikes = 0;
②定义m_prefab_sky_spikes,并动态资源加载
m_prefab_sky_spikes = Resources.Load("smashing_spikes") as GameObject;
使用协程生成路障移动动画
路障生成优化
生成地图片段的方法,第二个for循环也要进行“概率”判断。
for (int j = 0; j < 5; j++)
{
Vector3 pos = new Vector3(j * bottomLength + bottomLength / 2, 0, offsetZ + i * bottomLength + bottomLength / 2);
Vector3 rot = new Vector3(-90, 45, 0);
GameObject tile = null;
int pr = CalcPR();
if (pr == 0)
{
tile = GameObject.Instantiate(m_prefab_tile, pos, Quaternion.Euler(rot));
tile.GetComponent<Transform>().Find("normal_a2").GetComponent<MeshRenderer>().material.color = colorTwo;
tile.GetComponent<MeshRenderer>().material.color = colorTwo;
}
else if (pr == 1)
{
tile = new GameObject();
tile.GetComponent<Transform>().position = pos;
tile.GetComponent<Transform>().rotation = Quaternion.Euler(rot);
}
else if (pr == 2)
{
tile = GameObject.Instantiate(m_prefab_spikes, pos, Quaternion.Euler(rot));
}
else if (pr == 3)
{
tile = GameObject.Instantiate(m_prefab_sky_spikes, pos, Quaternion.Euler(rot));
}
tile.GetComponent<Transform>().SetParent(m_Transform);
item2[j] = tile;
}
路障动画
使用协程实现路障动画效果。
路障本身是两个模型,我们要控制子模型进行上下位移。
1>地面陷阱动画
上移:0.15f
动作步长:Time.deltaTime*10
①创建脚本Spikes,并拖入路障的预设体
②定义m_Transform并获取
③定义son_Transform并获取
④定义Vector3类型变量normalPos和targetPos变量,并赋值
normalPos = son_Transform.position;
targetPos = son_Transform.position + new Vector3(0, 0.15f, 0);
⑤定义两个协程函数Up(),Down()
private IEnumerator Up()
{
while(true)
{
son_Transform.position = Vector3.Lerp(son_Transform.position, targetPos, Time.deltaTime * 10);
yield return null;
}
}
private IEnumerator Down()
{
while(true)
{
son_Transform.position = Vector3.Lerp(son_Transform.position, normalPos, Time.deltaTime * 10);
yield return null;
}
}
private IEnumerator UpAndDown()
{
while (true)
{
StopCoroutine("Down");
StartCoroutine("Up");
yield return new WaitForSeconds(2.0f);
StopCoroutine("Up");
StartCoroutine("Down");
yield return new WaitForSeconds(2.0f);
}
}
⑥在Start函数中开启UpAndDown协程
2>天空陷阱动画
下移:0.6fI
动作步长:Time.deltaTime*20
创建脚本SkySpikes,并将Spikes脚本代码复制,将名字修改为天空陷阱的相关名字。
角色与路障交互死亡判断
路障蜗牛痕迹
当路障处于“安全状态”时,角色移动到路障上形成蜗牛痕迹。
操作步骤:
1创建三个tag标签,分别设置给路障和瓷砖模型;
2根据tag判断出“位置类型”,然后查找相应的地面模型进行蜗牛痕迹生成。
①添加三个tag,并且分给三个物体:
②修改SetPlayerPos():
void SetPlayerPos()
{
Transform PlayerPos = m_MapManager.mapList[z][x].GetComponent<Transform>();
MeshRenderer normal_a2 = null;
if(PlayerPos.tag == "Tile")
{
normal_a2 = PlayerPos.Find("normal_a2").GetComponent<MeshRenderer>();
}
else if(PlayerPos.tag == "Spikes")
{
normal_a2 = PlayerPos.Find("moving_spikes_a2").GetComponent<MeshRenderer>();
}
else if(PlayerPos.tag == "Sky_Spikes")
{
normal_a2 = PlayerPos.Find("smashing_spikes_a2").GetComponent<MeshRenderer>();
}
if(z % 2 == 0)
{
normal_a2.material.color = colorOne;
}
else
{
normal_a2.material.color = colorTwo;
}
m_Transform.position = PlayerPos.position + new Vector3(0, 0.254f / 2, 0);
m_Transform.rotation = PlayerPos.rotation;
}
死亡判断
何时死亡?
1被地面路障刺到;(触发判断)
2被天空路障刺到;(触发判断)
①定义bool变量life设置为true
②如果life为true才可以调用PlayerControl,即控制玩家
③添加函数GameOver():
private void GameOver()
{
life = false;
}
③为玩家添加盒状碰撞器,并勾选触发器选项
④为地面陷阱的子物体尖刺添加盒状碰撞器勾选触发器选项,同时添加Spikes_Attack的tag。并添加刚体组件,取消勾选使用重力。
对天空陷阱做相同操作
3掉入坑洞陷阱;(刚体下落)
①修改SetPlayerPos函数:
void SetPlayerPos()
{
Transform PlayerPos = m_MapManager.mapList[z][x].GetComponent<Transform>();
MeshRenderer normal_a2 = null;
m_Transform.position = PlayerPos.position + new Vector3(0, 0.254f / 2, 0);
m_Transform.rotation = PlayerPos.rotation;
if (PlayerPos.tag == "Tile")
{
normal_a2 = PlayerPos.Find("normal_a2").GetComponent<MeshRenderer>();
}
else if(PlayerPos.tag == "Spikes")
{
normal_a2 = PlayerPos.Find("moving_spikes_a2").GetComponent<MeshRenderer>();
}
else if(PlayerPos.tag == "Sky_Spikes")
{
normal_a2 = PlayerPos.Find("smashing_spikes_a2").GetComponent<MeshRenderer>();
}
if(normal_a2 != null)
{
if (z % 2 == 0)
{
normal_a2.material.color = colorOne;
}
else
{
normal_a2.material.color = colorTwo;
}
}
else
{
gameObject.AddComponent<Rigidbody>();
StartCoroutine("GameOver", true);
}
}
②将GameOver函数修改为协程函数:
private IEnumerator GameOver(bool b)
{
if(b)
{
yield return new WaitForSeconds(0.5f);
}
life = false;
Time.timeScale = 0;
}
4自己所在位置地面塌陷。(刚体下落)
地面塌陷时的代码添加如下代码:
游戏奖励物品生成与交互
优化代码:
修改PlayerController中的GameOver:
public IEnumerator GameOver(bool b)
{
if(b)
{
yield return new WaitForSeconds(0.5f);
}
if(life)
{
life = false;
m_CameraFollow.StartFollow = false;
}
}
奖励物体生成
奖励物品要在正常的瓷砖物体的上方生成,奖励物体的生成概率是一个固定值。
奖励物品自身要进行轴向的旋转。
奖励物品生成的位置在瓷砖上方0.06f。
①定义m_prefab_gem,并动态加载
②定义宝石的生成概率
private int pr_gem = 2;
③添加计算宝石生成概率的函数
/// <summary>
/// 计算宝石生成概率
/// </summary>
/// <returns>0:不生成,1:生成</returns>
private int CalcGemPR()
{
int pr = Random.Range(1, 100);
if(pr <= pr_gem)
{
return 1;
}
return 0;
}
④当生成瓷砖时有概率生成宝石
int gemPR = CalcGemPR();
if(gemPR == 1)
{
GameObject gem = GameObject.Instantiate(m_prefab_gem,tile.GetComponent<Transform>().position + new Vector3(0,0.06f,0),Quaternion.identity) as GameObject;
gem.GetComponent<Transform>().SetParent(tile.GetComponent<Transform>());
}
⑤创建Gem脚本,获取Transform组件。定义m_gem变量,获取宝石的子物体gem 3;
⑥旋转奖励物品
m_gem.Rotate(Vector3.up);
触发吃掉奖励物品
角色移动,与奖励物品接触,销毁奖励物体,然后增加角色奖励物品得分。
①定义gemCount变量,定义AddGemCount函数
private void AddGemCount()
{
gemCount++;
}
②对于gem预设体的子物体gem 3:添加盒状碰撞器勾选触发器,添加刚体组件,取消勾选使用重力,添加Gem的tag.
③修改onTriggerEnter函数
private void OnTriggerEnter(Collider coll)
{
if(coll.tag == "Spikes_Attack")
{
StartCoroutine("GameOver", false);
}
if(coll.tag == "Gem")
{
GameObject.Destroy(coll.gameObject.GetComponent<Transform>().parent.gameObject);
AddGemCount();
}
}
实现移动得分记录。
①定义scoreCount,并定义分数增加函数
private void AddScoreCount()
{
scoreCount++;
}
②当z++时,调用上述函数
PlayerPrefs存储游戏分数信息
PlayerPrefs
PlayerPrefs类是Unity提供的一个用于存储少量游戏数据的工具类,数据是以“键值对”的形式存储的。
数据在不同的平台存储的位置是不同的。
在windows 平台PlayerPrefs的数据是存储在注册表中的。
游戏分数信息存储
PlayerPrefs 存储位置
1win+R调出运行面板,输入“regedit”打开注册表面板;
2HKEY_CURRENT_USER – >SOFTWARE – >公司名 – >应用名。
①首先做一些设置:File->Build Settings->Player Settings
Company Name:MKCOOE
Product Name:CubeRun
宝石数存储
在游戏开始时,将宝石数读取出来,游戏结束时,再将新的宝石数存储回去。
②在PlayerController中的start中添加代码:
gemCount = PlayerPrefs.GetInt("gem", 0);
③定义存储数据函数:
private void SaveData()
{
PlayerPrefs.SetInt("Gem", gemCount);
}
④调用该函数:
当游戏结束时调用。
分数(最高分)存储
游戏结束时,将当前的得分与存储中的分数进行比较,如果高于原来的得分,就将新的晶高分存储回去。
⑤修改SaveData函数
private void SaveData()
{
PlayerPrefs.SetInt("gem", gemCount);
if(scoreCount > PlayerPrefs.GetInt("score",0))
{
PlayerPrefs.SetInt("score", scoreCount);
}
}
游戏UI界面逻辑【上】
UI界面逻辑
①创建UIManager脚本将该脚本拖到UI Root,删除shop_UI(先不做该UI)
1>开始与游戏界面隐藏与显示切换;
②声明并获取,m_StartUI和m_GameUi,将两种UI手动激活,将m_GameUI用代码失活:
m_StartUI = GameObject.Find("Start_UI");
m_GameUI = GameObject.Find("Game_UI");
m_GameUI.SetActive(false);
2>开始界面显示宝石数以及最高分数;
①将此图所在标签更名为Score_Label
②将此图所在标签更名为Gem_Label
③定义m_ScoreLabel和m_GemLabel并获取
m_ScoreLabel = GameObject.Find("Score_Label").GetComponent<UILabel>();
m_GemLabel = GameObject.Find("Gem_Label").GetComponent<UILabel>();
④定义初始化函数:
private void Init()
{
m_ScoreLabel.text = PlayerPrefs.GetInt("score", 0) + "";
m_GemLabel.text = PlayerPrefs.GetInt("gem", 0) + "/100";
}
3>游戏界面初始化宝石数与分数;
①更名
②定义获取
private UILabel m_GameScoreLabel;
private UILabel m_GameGemLabel;
m_GameScoreLabel = GameObject.Find("GameScoreLabel").GetComponent<UILabel>();
m_GameGemLabel = GameObject.Find("GameGemLabel").GetComponent<UILabel>();
③修改初始化函数
private void Init()
{
m_ScoreLabel.text = PlayerPrefs.GetInt("score", 0) + "";
m_GemLabel.text = PlayerPrefs.GetInt("gem", 0) + "/100";
m_GameScoreLabel.text = "0";
m_GameGemLabel.text = PlayerPrefs.GetInt("gem", 0) + "/100";
}
4>使用委托给开始按钮绑定点击事件处理方法;
①添加盒状碰撞器,并且附加Button Script
②定义并获取
private GameObject m_PlayButton;
m_PlayButton = GameObject.Find("play_btn");
③添加委托
UIEventListener.Get(m_PlayButton).onClick = PlayButtonClick;
private void PlayButtonClick(GameObject go)
{
Debug.Log("游戏开始");
}
④修改PlayButtonClick:
private void PlayButtonClick(GameObject go)
{
Debug.Log("游戏开始");
m_StartUI.SetActive(false);
m_GameUI.SetActive(true);
}
}
⑤将PlayerController脚本中做如下修改:
⑥定义并获取:
m_PlayerController = GameObject.Find("cube_dice").GetComponent<PlayerController>();
⑦修改PlayerButtonClick:
private void PlayButtonClick(GameObject go)
{
Debug.Log("游戏开始");
m_StartUI.SetActive(false);
m_GameUI.SetActive(true);
m_PlayerController.GameStart();
}
5>游戏界面更新分数UI和宝石数UI;
①在UIManager脚本中添加函数:
public void UpdateData(int score,int gem)
{
m_GameScoreLabel.text = score.ToString();
m_GameGemLabel.text = gem + "/100";
}
②定义获取
m_UIManager = GameObject.Find("UI Root").GetComponent<UIManager>();
③调用函数
private void AddGemCount()
{
gemCount++;
m_UIManager.UpdateData(scoreCount, gemCount);
}
private void AddScoreCount()
{
scoreCount++;
m_UIManager.UpdateData(scoreCount, gemCount);
Debug.Log("分数:" + scoreCount);
}
游戏UI界面逻辑【下】
1>角色死亡后等待2秒然后UI界面跳转回开始界面;
①在UIManager中添加函数
public void ResetUI()
{
m_StartUI.SetActive(true);
m_GameUI.SetActive(false);
}
②在PlayerController中添加协程:
private IEnumerator ResetGame()
{
yield return new WaitForSeconds(2);
m_UIManager.ResetUI();
}
③生命结束时,开启协程
2>角色死亡后更新开始界面的分数;
修改UIManager:UpdateData函数
public void UpdateData(int score,int gem)
{
m_GemLabel.text = gem + "/100";
m_GameScoreLabel.text = score.ToString();
m_GameGemLabel.text = gem + "/100";
}
3>重置角色:
取消角色的刚体组件;
在PlayerController中添加函数ResetPlayer:
public void UpdateData(int score,int gem)
{
m_GemLabel.text = gem + "/100";
m_GameScoreLabel.text = score.ToString();
m_GameGemLabel.text = gem + "/100";
}
在ResetGame中调用
重置出生点位置;
修改ResetPlayer()
private void ResetPlayer()
{
GameObject.Destroy(gameObject.GetComponent<Rigidbody>());
z = 3;
x = 2;
}
重置 life值;
private void ResetPlayer()
{
GameObject.Destroy(gameObject.GetComponent<Rigidbody>());
z = 3;
x = 2;
life = true;
}
重置当前分数(UI也要重置);
private void ResetPlayer()
{
GameObject.Destroy(gameObject.GetComponent<Rigidbody>());
z = 3;
x = 2;
life = true;
scoreCount = 0;
}
public void ResetUI()
{
m_StartUI.SetActive(true);
m_GameUI.SetActive(false);
m_GameScoreLabel.text = "0";//UI重置
}
4>重置场景:
删掉现有的所有场景元素;
在MapManager脚本中添加函数:
public void ResetGameMap()
{
Transform[] sonTransform = m_Transform.GetComponentsInChildren<Transform>();
for(int i = 1;i < sonTransform.Length;i++)
{
GameObject.Destroy(sonTransform[i].gameObject);
}
}
重置相关的概率值;
public void ResetGameMap()
{
Transform[] sonTransform = m_Transform.GetComponentsInChildren<Transform>();
for(int i = 1;i < sonTransform.Length;i++)
{
GameObject.Destroy(sonTransform[i].gameObject);
}
pr_hole = 0;
pr_spikes = 0;
pr_sky_spikes = 0;
pr_gem = 2;
}
重置塌陷角标;
public void ResetGameMap()
{
Transform[] sonTransform = m_Transform.GetComponentsInChildren<Transform>();
for(int i = 1;i < sonTransform.Length;i++)
{
GameObject.Destroy(sonTransform[i].gameObject);
}
pr_hole = 0;
pr_spikes = 0;
pr_sky_spikes = 0;
pr_gem = 2;
index = 0;
}
清空List集合;
public void ResetGameMap()
{
Transform[] sonTransform = m_Transform.GetComponentsInChildren<Transform>();
for(int i = 1;i < sonTransform.Length;i++)
{
GameObject.Destroy(sonTransform[i].gameObject);
}
pr_hole = 0;
pr_spikes = 0;
pr_sky_spikes = 0;
pr_gem = 2;
index = 0;
mapList.Clear();
}
重新生成新的地图;
public void ResetGameMap()
{
Transform[] sonTransform = m_Transform.GetComponentsInChildren<Transform>();
for(int i = 1;i < sonTransform.Length;i++)
{
GameObject.Destroy(sonTransform[i].gameObject);
}
pr_hole = 0;
pr_spikes = 0;
pr_sky_spikes = 0;
pr_gem = 2;
index = 0;
mapList.Clear();
CreateMapItem(0);
}
ResetGame中调用该协程
private IEnumerator ResetGame()
{
yield return new WaitForSeconds(2);
ResetPlayer();
m_MapManager.ResetGameMap();
m_UIManager.ResetUI();
}
5>重置摄像机:
I
重置摄像机位置;
在CameraFollow脚本中
①定义当前位置,并初始当前位置
private Vector3 normalPos;
normalPos = m_Transfrom.position;
②添加函数ResetCamera
public void ResetCamera()
{
m_Transfrom.position = normalPos;
}
③ResetGame协程中调用:
private IEnumerator ResetGame()
{
yield return new WaitForSeconds(2);
ResetPlayer();
m_MapManager.ResetGameMap();
m_CameraFollow.ResetCamera();
m_UIManager.ResetUI();
}
Android触屏操作与APK细节配置
触屏操作
NGUI 中的Button是可以接受屏幕触屏事件的,我们只需要在GameUI 使用
两个“透明按钮”来模拟点击区域就可以实现该功能。
操作步骤:
1GameUI 界面制作两个按钮;
附加盒状碰撞器和Button脚本
2分离A和D的按键执行代码;
3两个按钮绑定对应的按键执行代码;
①定义获取:
m_Left = GameObject.Find("Left");
m_Right = GameObject.Find("Right");
②按键绑定
m_Left = GameObject.Find("Left");
UIEventListener.Get(m_Left).onClick = Left;
m_Right = GameObject.Find("Right");
UIEventListener.Get(m_Right).onClick = Right;
private void Left(GameObject go)
{
m_PlayerController.Left();
}
private void Right(GameObject go)
{
m_PlayerController.Right();
}
4将两个按钮进行透明化处理。
APK细节配置
1.切换平台为Android平台,并将场景加入打包队列
Unity 会对项目资源进行相应的“格式转换”。
点击Add Open Scenes,并更改分辨率为480*854
2.设置 Bundle Identifier
APK的唯一标识符,一般是公司域名反写+应用名称。
比如:net.mkcode.monkeycuberun
要想正确打包出APK,电脑上必须安装JDK和SDK,并配置好Unity 对着
两个工具的引用。