2.5 游戏界面
在上一节中已经介绍过了主菜单界面的开发过程,本节将要介绍的游戏界面是本游戏开发的中心场景,其他的场景都是为此场景服务的,游戏场景的开发对于此游戏的可玩性有至关重要的作用。在本节中,将对此界面的开发进行进一步的介绍。
2.5.1 场景的搭建
搭建游戏界面场景的步骤比较繁琐,由于篇幅的限制这里不能很详细地介绍每一个细节,所以,要求读者对Unity的基础知识有一定的了解。接下来对游戏界面的开发步骤进行具体的介绍。
(1)新建场景。选择“File”→“New Scene”,然后选择“File”→“Save Scene”选项(或者Ctrl+S),在保存对话框中输入场景名为“GameScene”,如图2-39所示。
▲图2-39 新建场景
(2)创建定向光源。选择“GameObject”→“Create Other”→“Directional Light”后会自动创建一个定向光源,如图2-40所示。调整定向光源位置,如图2-41所示。定向光源“Color”选项,为其选择适当颜色,如图2-42所示。
▲图2-40 创建定向光源
▲图2-41 定向光源位置摆放
▲图2-42 定向光源颜色选择
(3)创建点光源。选择“GameObject”→“Create Other”→“Point Light”后会自动创建一个点光源,如图2-43所示。调整点光源位置,如图2-44所示。
▲图2-43 创建点光源
▲图2-44 点光源摆放位置
(4)导入房间和球桌模型。将房间模型拖入游戏场景,然后调整位置、姿态、大小,如图2-45所示。然后将球桌模型拖入游戏场景,置于房间模型中央,调整位置、姿态、大小,如图2-46所示。
▲图2-45 房间模型摆放
▲图2-46 球桌模型摆放
(5)为球桌贴纹理。将球桌纹理拖曳到桌球模型上,如图2-47所示。其中,桌案的不同部位需要不同材质,需要分别贴图,由于桌案组成部分复杂,各个部分贴图不再一一赘述。
▲图2-47 球桌贴纹理
(6)导入球杆模型。将球杆模型拖入游戏场景,然后调整位置、姿态、大小,如图2-48所示。然后将球杆模型拖入游戏场景,置于房间模型中央,调整位置、姿态、大小,如图2-49所示。
▲图2-48 球杆模型拖入场景
▲图2-49 球杆摆放位置
(7)制作桌球预制件。首先移除“Ball”球体对象的“Mesh Renderer”组件,然后将“plan”游戏对象拖动到“Ball”对象下变成父子关系。然后将“Ball”对象直接拖动到“Project”视图中的“Assets/Prefab”文件夹下,这样桌球预制件就制作成功了,如图2-50所示。
▲图2-50 桌球预制件
(8)添加球体碰撞器。由于球桌需发生碰撞并反弹,所以要为其添加球体碰撞器。首先选中“Ball”游戏对象,然后选择“Component”→“Physics”→“Sphere Collider”,Material则选择为Bouncy。具体情况如图2-51所示。
▲图2-51 添加球体碰撞器
(9)创建球洞平面。选择“GameObject”→“Create Other”→“Plane”选项,如图2-52所示。然后调整平面位置、姿态和大小,并命名为“BlackPlane”,置于“Table”游戏对象内。由于球桌共六个球洞,这里只举一例。其各项参数如图2-53所示。
▲图2-52 创建平面
▲图2-53 球洞平面参数
(10)添加盒子碰撞器。由于球桌的围栏需要阻挡并反弹桌球,所以要为其添加盒子碰撞器。首先选中“Table”游戏对象中的“Box001”—“Box007”和“CubeB”,然后选择“Component”→“Physics”→“Box Collider”,具体情况如图2-54所示。
▲图2-54 添加盒子碰撞器
(11)添加刚体。首先选中“CubeB”对象,然后选择“Component”→“Physics”→“Rigidbody”选项,具体情况如图2-55所示。
▲图2-55 添加刚体
(12)添加粒子系统。选择“GameObject”→“Create Other”→“Particle System”选项,如图2-56所示。然后调整平面位置、姿态和大小,并命名为“GlowBall”,置于“AssistBall”游戏对象内,其各项参数如图2-57所示。
▲图2-56 添加粒子系统
▲图2-57 粒子系统对象赋值
2.5.2 多视角的制作与切换
上一小节已经介绍了游戏界面的搭建过程,本小节将向读者介绍多视角的制作与切换。本游戏中主要有三个摄像机,游戏运行时,摄像机根据玩家切换视角的情况只有一个处于激活状态。具体的开发步骤如下。
(1)制作主摄像机。调整主摄像机“Main Camera”摄像机到适当位置,并设置“Field of View”属性值为30,去掉属性查看器中对象名称前单选框中的对勾,如图2-58所示。
▲图2-58 制作主摄像机
(2)制作第一人称摄像机。首先选择“GameObject”→“Create Other”→“Camera”新建一个摄像机对象,命名为“CameraOfFirstView”。然后调整摄像机到适当位置,可调整“Field of View”属性值为30,如图2-59所示。
▲图2-59 制作第一人称摄像机
(3)设置标签。首先选中第一人称摄像机对象,再选择属性查看器中的“Tag”→“Add Tag...”选项,如图2-60所示。然后会打开“TagManager”视图,在“Tags”选项下的“Element0”后填入“cam”,如图2-61所示。其他标签的添加,步骤相似,这里不再赘述。
▲图2-60 添加标签
▲图2-61 输入标签名称
(4)制作自由视角摄像机。首先选择“GameObject”→“Create Other”→“Camera”新建一个摄像机对象,命名为“CameraOfFreeView”,并将其标签修改为“cam”。然后调整摄像机到适当位置,可调整“Field of View”属性值为30。最后,去掉属性查看器中对象名称前单选框中的对勾,如图2-62所示。
▲图2-62 制作自由视角摄像机
(5)编写摄像机切换控制脚本。该脚本主要负责摄像机的切换,包括第一人称视角和第三人称视角,同时负责切换后摄像机角度和位置的恢复,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CamControl.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class CamControl : MonoBehaviour { 4 public LayerMask mask = -1; 5 public GameObject cueBall; 6 private float total_RotationX; //饶X轴的旋转角度 7 public float freeViewRotationMatrixY = 0; //free视角时绕Y轴的旋转角度 8 Logic logic; //获取组件 9 public GameObject []cameras; //摄像机数组 10 public static int curCam = 1; //当前摄像机编号 11 public static Vector3 prePosition = Vector3.zero; //初始化上一次触控点的位置 12 public static bool touchFlag = true; //是否允许触控的标志位 13 Matrix4x4 inverse; //GUI的逆矩阵 14 Quaternion qua; //记录初始旋转位置的变量 15 Vector3 vec; //记录初始位置的变量 16 void Start () { 17 qua = cameras[4].transform.rotation; //记录初始位置,主要是用于恢复位置 18 vec = cameras[4].transform.position; 19 for (int i = 0; i < 3; i++){ //进行自适应 20 cameras[i].camera.aspect = 800.0f / 480.0f; //设置视口的缩放比 21 float lux = (Screen.width - ConstOfMenu.desiginWidth * 22 Screen.height / ConstOfMenu.desiginHeight) / 2.0f; //计算视口的GUI矩阵 23 cameras[i].camera.pixelRect = new Rect(lux, 0, Screen.width -2 * lux, Screen.height); 24 } 25 total_RotationX = 13; //设置旋转角度为13度 26 logic = GetComponent("Logic") as Logic; //获取脚本组件 27 inverse = ConstOfMenu.getInvertMatrix(); //获取逆矩阵 28 } 29 public void ChangeCam(int index) 30 { 31 setFreeCame(); //每次切换时都调用恢复数据方法 32 cameras[curCam].SetActive(false); //设置当前摄像机不可用 33 cameras[index].SetActive(true); //启用相应摄像机 34 curCam = index; //设置当前摄像机索引 35 } 36 public void moveCame(int sign) //对应于gameLayer类中的far与near按钮 37 { 38 cameras[curCam].transform.Translate(new Vector3(0, 0, sign * Time.deltaTime)); 39 Vector3 posCueBall= 40 cameras[curCam].transform.InverseTransformPoint(cueBall.transform. position); 41 if (posCueBall.z > 35 || posCueBall.z < 7) { //设置移动的最大距离与最新记录 42 cameras[curCam].transform.Translate(new Vector3(0, 0, -sign * Time.deltaTime)); 43 } 44 } 45 ……//此处省略了Update方法,将在下面进行介绍 46 ……//此处省略了setFreeCame方法,将在下面进行介绍 47 ……//此处省略了mainFunction方法,将在下面进行介绍 48 ……//此处省略了firstFunction方法,将在下面进行介绍 49 ……//此处省略了freeFunction方法,将在下面进行介绍 50 }
第4行~第15行主要声明关于摄像机位置变换和角度变换的变量,声明摄像机初始位置变量以及为每个摄像机编号,方便切换处理。
第16行~第28行主要负责记录摄像机的初始位置,方便后续恢复角度和位置的处理。同时进行自适应的相关计算和处理。
第29行~第35行为切换摄像机方法,每次切换完,都要对相应摄像机进行恢复初始位置角度。同时关闭当前摄像机,启用相应的摄像机,并记录该摄像机索引。
第36行~第44行主要负责摄像机角度的移动变换,该片段主要对应游戏主界面的far和near按钮,当用户单击这两个按钮的时候,摄像机会进行匀速移动,调整摄像机位置,或远或近。
(6)下面介绍上述代码省略的Update方法以及setFreeCame方法。前者主要负责当玩家滑动屏幕时,摄像机角度的旋转。后者主要负责切换了视角之后,重新设置摄像机的各种信息。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CamControl.cs。
1 void Update () { 2 if (!touchFlag) { //如果不触控 3 return; 4 } 5 if (!GameLayer.TOTAL_FLAG) { //如果允许触控 6 return; 7 } 8 if (Input.GetMouseButton(0)) { //手指滑动时的回调方法 9 float angleY=(Input.mousePosition.x - prePosition.x)/ 10 ConstOfGame.SCALEX; //计算绕Y轴的旋转角度 11 float angleX=(Input.mousePosition.y - prePosition.y) / 12 ConstOfGame.SCALEY; //计算绕X轴的旋转角度 13 Vector3 newPoint= 14 ConstOfMenu.getInvertMatrix().MultiplyVector(Input. mousePosition); 15 switch (curCam) { //不同的摄像机执行不同的方法 16 case 0: mainFunction(Input.mousePosition); break; 17 case 1: firstFunction(angleY, angleX); break; 18 case 2: freeFunction(angleY, angleX); break; 19 } 20 prePosition = Input.mousePosition; //记录上一次的触控位置 21 }} 22 public void setFreeCame(){ //重新设置各个摄像机的各种信息 23 cameras[3].transform.rotation = cameras[4].transform.rotation; 24 cameras[3].transform.position = cameras[4].transform.position; 25 freeViewRotationMatrixY = GameLayer.totalRotation; 26 total_RotationX = 13; //重新设置旋转角度的度数 27 cameras[2].transform.position = cameras[4].transform.position; 28 cameras[2].transform.rotation = cameras[4].transform.rotation; 29 cameras[1].transform.position = cameras[4].transform.position; 30 cameras[1].transform.rotation = cameras[4].transform.rotation; 31 }
第8行~第14行为手指滑动时的回调方法,根据用户触控的位置,计算摄像机绕x和y轴的旋转角度。
第15行~第20行表示不同的摄像机执行不同的旋转方法,根据当前摄像机编号,调用各自方法。同时需要记录上一次触控位置,方便后续操作使用。
第22行~第30行主要负责重新设置各个摄像机的各种信息,其中包括摄像机的位置和角度的恢复。
(7)下面介绍上述代码省略的mainFunction方法、firstFunction方法以及freeFunction方法。这三个方法分别负责主摄像机、第一人称视角摄像机和第三人称视角摄像机的旋转操作。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CamControl.cs。
1 void mainFunction(Vector3 pos) 2 { 3 RaycastHit hit; 4 Ray ray = Camera.main.ScreenPointToRay(pos); 5 if (Physics.Raycast(ray, out hit, 100, mask.value)) { 6 cameras[3].transform.rotation = qua; //重置位置以及旋转角度 7 cameras[3].transform.position = vec; 8 Vector3 hitPoint = hit.point; //获取碰撞点坐标 9 Vector3 cubBallPoint = cueBall.transform.position; //获取与球台交点坐标 10 float angle=180- Mathf.Atan2(cubBallPoint.x - hitPoint.x,//计算旋转角度 11 cubBallPoint.z - hitPoint.z) * Mathf.Rad2Deg; 12 GameLayer.totalRotation = -angle; //计算总的旋转角度 13 cameras[3].transform.transform.RotateAround(ConstOfGame.CUEBALL_POSITION, 14 Vector3.up, GameLayer.totalRotation); 15 logic.cueObject.transform.rotation= 16 cameras[3].transform.rotation; //设置球杆的旋转角度 17 } 18 void firstFunction(float angleY,float angleX) //第一人称视角的回调方法 19 { 20 if (Mathf.Abs(angleY) > Mathf.Abs(angleX) && Mathf.Abs(angleY)>1f) { 21 GameLayer.totalRotation += angleY; //计算Y的旋转角度 22 logic.cueObject.transform.RotateAround( 23 logic.cueBall.transform.position, Vector3.up, angleY); //设置球杆的旋转角度 24 }else{ 25 if (total_RotationX + angleX > 10 && total_RotationX + angleX < 90){ 26 if (Mathf.Abs(angleX) > 1f){ 27 Vector3 right=new Vector3(Mathf.Cos(-GameLayer.totalRotation / 28 180.0f * Mathf.PI),0, Mathf.Sin(-GameLayer. totalRotation / 29 180.0f * Mathf.PI)); //计算旋转轴 30 total_RotationX += angleX; //计算X轴的总旋转角度 31 cameras[1].transform.RotateAround( 32 logic.cueBall.transform.position, right, angleX); //摄像机旋转 33 }}}} 34 void freeFunction(float angleY, float angleX) //第三人称视角的回调方法 35 { 36 if (Mathf.Abs(angleY) > 0.5f) { 37 freeViewRotationMatrixY += angleY; //计算Y的旋转角度 38 cameras[2].transform.RotateAround( 39 logic.cueBall.transform.position, Vector3.up, angleY);//设置球杆的旋转角度 40 }else{ 41 if (total_RotationX + angleX > 10 && total_RotationX + angleX < 90f) { 42 Vector3 right = 43 cameras[curCam].transform.TransformDirection(Vector3.right); //计算旋转轴 44 total_RotationX += angleX; //计算X轴的总旋转角度 45 cameras[2].transform.RotateAround( 46 logic.cueBall.transform.position, right, angleX); //摄像机旋转 47 }}}
第1行~第17行主要为主摄像机视角的角度旋转方法,获取碰撞点坐标,获取碰撞点与球台的交点坐标,然后利用该点坐标与白球坐标连线重新计算球杆的旋转角度。最后重新设置球杆的旋转角度。
第18行~第33行主要表示第一人称视角的回调方法,为了呈现第一人称更好的视觉效果,y轴旋转没有限制,x轴旋转在0~90度之间。将球杆旋转后,为摄像机定义旋转角度。
第34行~第47行主要表示第三人称视角的回调方法,计算x轴的总旋转角度,总旋转角度主要是限制旋转的幅度。将球杆旋转后,为摄像机定义旋转角度。
2.5.3 游戏界面脚本的编写
上一小节已经介绍了游戏场景的制作过程,本小节将向读者介绍游戏界面绘制构造的相关脚本。每个脚本负责游戏界面不同的部分,详细的编写介绍如下。
(1)编写游戏界面脚本。该脚本主要负责绘制游戏主界面,包括游戏主界面的多个按钮等,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的GameLayer.cs。
1 using UnityEngine; 2 using System.Collections; 3 using System.Collections.Generic; 4 public class GameLayer : MonoBehaviour 5 { 6 enum ButtonS{ //声明按钮系列 7 Go = 0, Far, Near, Left, Right, M, firstV, freeV, thirdV} 8 public static ArrayList BallGroup_ONE_EIGHT = new ArrayList(); //八球模式下的2个辅助列表 9 public static ArrayList BallGroup_TWO_EIGHT = new ArrayList(); 10 public static ArrayList BallGroup_ONE_NINE = new ArrayList(); //9球模式下的一个辅助列表 11 public static ArrayList BallGroup_TOTAL = new ArrayList(); //所有球的列表 12 public static int ballInNum = 0; //进球个数 13 public static float totalRotation = 0.0f; //饶Y轴的总旋转角度 14 public static bool TOTAL_FLAG = true; //触控与按钮是否可用的总标志位 15 public static bool isStartAction = false; //球杆是否运动的总标志位 16 private bool isFirstView; //左下角显示按钮的标志位 17 private bool isFirstActionOver; //第一次运动是否结束的标志位 18 private bool isSecondActionOver; //第二次运动是否结束的标志位 19 private int tbtIndex; //控制右下角图片的变量 20 Matrix4x4 guiMatrix; //gui的自适应矩阵 21 public GUIStyle[] btnStyle; //按钮的Style 22 public GUIStyle fbtnStyle; //按钮的GUIStyle 23 Logic logic; //主逻辑类组件 24 MiniMap miniMap; //小地图组件 25 InitAllBalls initClass; //初始化桌球的组件 26 public Texture2D[] nums; 27 public AudioClip startSound; //进球的音效 28 void Start() 29 { 30 isFirstView = true; //左下角显示按钮的标志位 31 isFirstActionOver = false; //第一次运动是否结束的标志位 32 isSecondActionOver = false; //第二次运动是否结束的标志位 33 GameLayer.BallGroup_TOTAL.Add(GameObject.Find("CueBall")); //首先将母球添加进总列表 34 initClass = GetComponent("InitAllBalls") as InitAllBalls; 35 miniMap = GetComponent("MiniMap") as MiniMap; 36 initClass.initAllBalls(PlayerPrefs.GetInt("billiard")); //初始化所有的桌球 37 logic = GetComponent("Logic") as Logic; //获取该组件 38 if (PlayerPrefs.GetInt("offMusic") != 0){ //播放背景音乐的判断 39 audio.Pause(); //播放背景音乐 40 } 41 guiMatrix = ConstOfMenu.getMatrix(); //获取GUI矩阵 42 } 43 void Update(){ 44 if (Input.GetKeyDown(KeyCode.Escape)) { //如果按下的是返回键 45 Application.LoadLevel("MenuScene"); //加载LevelSelectScene场景 46 }} 47 ……//此处省略了OnGUI方法,将在下面进行介绍 48 ……//此处省略了resetAllStaticData方法,将在下面进行介绍 49 ……//此处省略了DrawButtons方法,将在下面进行介绍 50 ……//此处省略了cueRunAction方法,将在下面进行介绍 51 }
第6第~第11行主要声明了游戏主界面上的各个按钮,声明了八球游戏模式下的全色球和花色球的两个辅助列表,九球游戏模式下的桌球辅助列表以及所有桌球的一个辅助列表。
第12行~第27行主要声明了游戏相关变量和相关的多种标志位,其中包括进球个数、按钮样式等。同时声明了三个脚本组件,包括主控逻辑组件、游戏小地图组件和初始化桌球组件,这些组件在主界面的绘制构造上起到一定的作用,将在后面进行详细介绍。
第28行~第42行主要为Start方法,首先对所需的标志位进行设置,然后将母球添加进桌球总列表,同时设置背景音乐的播放。
第43行~第46行表示当用户按下返回键的时候,退出游戏界面,加载游戏模式选择界面。
(2)下面介绍上述代码省略的DrawButtons方法,该方法主要负责游戏界面各个按钮的绘制。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的GameLayer.cs。
1 void DrawButtons() //绘制按钮方法 2 { 3 if (GUI.Button(new Rect(0, ConstOfGame.btnPositonY, //Go按钮 4 ConstOfGame.btnSize, ConstOfGame.btnSize), "", btnStyle[(int)ButtonS. Go])) { 5 if (GameLayer.TOTAL_FLAG) { //如果允许按钮起作用 6 GameLayer.TOTAL_FLAG = false; 7 isFirstActionOver = false; //第一次运动是否结束的标志位 8 isSecondActionOver = false; //第二次运动是否结束的标志位 9 GameLayer.isStartAction = true; //设置移动的标志位 10 logic.cuePosition = logic.cueBall.transform.position; 11 }} 12 if (GUI.RepeatButton(new Rect(100, ConstOfGame.btnPositonY, //缩小按钮 13 ConstOfGame.btnSize, ConstOfGame.btnSize), "", btnStyle[(int)ButtonS.Far])){ 14 if (GameLayer.TOTAL_FLAG) { //如果允许按钮起作用 15 (GetComponent("CamControl") as CamControl).moveCame(-5); 16 }} 17 if (GUI.RepeatButton(new Rect(200, ConstOfGame.btnPositonY, //放大按钮 18 ConstOfGame.btnSize, ConstOfGame.btnSize), "", btnStyle[(int)ButtonS.Near])){ 19 if (GameLayer.TOTAL_FLAG) { //如果允许按钮起作用 20 (GetComponent("CamControl") as CamControl).moveCame(5); 21 }} 22 if (GUI.RepeatButton(new Rect(300, ConstOfGame.btnPositonY, //左旋转按钮 23 ConstOfGame.btnSize,ConstOfGame.btnSize),"",btnStyle[(int)ButtonS.Left])){ 24 if (GameLayer.TOTAL_FLAG) { //如果允许按钮起作用 25 GameLayer.totalRotation -= ConstOfGame.rotationStep; //计算总的旋转角度 26 logic.cueObject.transform.RotateAround(logic.cueBall.transform.position, 27 Vector3.up, -ConstOfGame.rotationStep); //设置球杆的旋转角度 28 }} 29 if (GUI.RepeatButton(new Rect(460, ConstOfGame.btnPositonY, //右旋转按钮 30 ConstOfGame.btnSize,ConstOfGame.btnSize),"",btnStyle[(int)ButtonS.Right])){ 31 if (GameLayer.TOTAL_FLAG){ //如果允许按钮起作用 32 GameLayer.totalRotation += ConstOfGame.rotationStep; //计算总的旋转角度 33 logic.cueObject.transform.RotateAround(logic.cueBall.transform. position, 34 Vector3.up, ConstOfGame.rotationStep); //设置球杆的旋转角度 35 }} 36 if (GUI.Button(new Rect(550, ConstOfGame.btnPositonY, //M按钮 37 ConstOfGame.btnSize, ConstOfGame.btnSize), "", btnStyle[(int)ButtonS.M])){ 38 if (GameLayer.TOTAL_FLAG){ //如果允许按钮起作用 39 MiniMap.isMiniMap = !MiniMap.isMiniMap; //重新设置绘制小地图的标志位 40 }} 41 if (GUI.Button(new Rect(650, ConstOfGame.btnPositonY, //F按钮 42 ConstOfGame.btnSize, ConstOfGame.btnSize), "", fbtnStyle)){ 43 if (GameLayer.TOTAL_FLAG){ //如果允许按钮起作用 44 logic.assistBall.SetActive(!logic.assistBall.activeSelf); //调用辅助线 45 logic.line.SetActive(!logic.line.activeSelf); 46 }} 47 if (isFirstView){ 48 if (GUI.Button(new Rect(740, ConstOfGame.btnPositonY, 49 ConstOfGame.btnSize, ConstOfGame.btnSize), "", btnStyle[(int)ButtonS.firstV])){ 50 if (GameLayer.TOTAL_FLAG){ //如果允许按钮起作用 51 (GetComponent("CamControl") as CamControl).ChangeCam(2); 52 isFirstView = !isFirstView; 53 }}}else { 54 if (GUI.Button(new Rect(740, ConstOfGame.btnPositonY, 55 ConstOfGame.btnSize, ConstOfGame.btnSize), "", btnStyle[(int)ButtonS.freeV])){ 56 if (GameLayer.TOTAL_FLAG){ //如果允许按钮起作用 57 (GetComponent("CamControl") as CamControl).ChangeCam(1); 58 isFirstView = !isFirstView; 59 }}} 60 if (GUI.Button(new Rect(730, 10, 30, 30), "", btnStyle[(int)ButtonS.thirdV])){ 61 if (GameLayer.TOTAL_FLAG){ //如果允许按钮起作用 62 (GetComponent("CamControl") as CamControl).ChangeCam(0); 63 }}}
第3行~第11行绘制了GO按钮的时候,当用户单击该按钮的时候,母球进行击打。由于击打过程中,屏幕不允许触碰,摄像机不移动,所以对各个标志位进行设置。
第12行~第21行绘制了视野缩小、放大两个按钮,用户单击视野缩小和放大两个按钮的时候,每单击一次,摄像机移动固定距离,调整视野的远近,也可以说是对游戏界面内实物显示大小的调整。
第22行~第35行绘制了左旋转和右旋转按钮。用户单击该按钮的时候,会使球杆进行一定角度的旋转,从而调整击球位置。
第36行~第46行绘制了M按钮和F按钮,分别代表游戏界面左上角的小地图显示按钮,以及母球和目标球之间的辅助线显示按钮。用户单击这两个按钮的时候,进行相应切换。
第47行~第62行绘制了游戏视觉切换按钮。单击最右下角视角按钮,切换视角。默认视角为第一人称视角,单击该按钮切换到第三人称视角,玩家可抹动屏幕全方位观看游戏场景。单击右上角进球个数按钮,切换至俯视视角。用户可根据自己的喜好,进行相应的调整。
(3)下面介绍上述代码省略的OnGUI方法、resetAllStaticData方法以及cueRunAction方法。这3个方法分别负责游戏界面相应内容的绘制,游戏界面相关变量的数据恢复重置和母球运动状态控制。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的GameLayer.cs。
1 void OnGUI() 2 { 3 GUI.matrix = guiMatrix; //设置GUI的矩阵 4 GUI.DrawTexture(new Rect(770, 10, 30, 30), nums[GameLayer.ballInNum]); //绘制提示UI 5 DrawButtons(); //绘制按钮 6 miniMap.drawMiniMap(); //绘制mini地图 7 if (GameLayer.isStartAction) //如果允许球杆运动 8 { 9 cueRunAction(); //球杆执行动作 10 }} 11 public static void resetAllStaticData(){ //重置数据 12 BallGroup_ONE_EIGHT.Clear(); //清空八球模式两个列表 13 BallGroup_TWO_EIGHT.Clear(); 14 BallGroup_ONE_NINE.Clear(); //清空九球模式辅助列表 15 BallGroup_TOTAL.Clear(); //清空桌球总列表 16 ballInNum = 0; //进球个数 17 totalRotation = 0.0f; //饶Y轴的总旋转角度 18 TOTAL_FLAG = true; //触控与按钮是否可用的总标志位 19 isStartAction = false; //球杆是否运动的总标志为 20 CamControl.curCam = 1; //相机索引 21 CamControl.prePosition = Vector3.zero; //上一次的触控位置 22 CamControl.touchFlag = true; //触控的标志位 23 PowerBar.showTime = 720; //剩余时间 24 PowerBar.restBars = 22; //能量条格子大小 25 } 26 } 27 void cueRunAction() 28 { 29 if (!isFirstActionOver) { //如果第一次没有运动完 30 logic.cue.transform.Translate(new Vector3(0, 0, Time.deltaTime)); 31 if (logic.cue.transform.localPosition.z <= -2){ 32 isFirstActionOver = true; //第一次运动完成 33 }}else if (!isSecondActionOver && isFirstActionOver) { //第一次运动结束,第二次运动没有结束 34 logic.cue.transform.Translate(new Vector3(0, 0, -2 * Time.deltaTime)); 35 if (logic.cue.transform.localPosition.z >= -0.45f){ 36 isSecondActionOver = true; //第一次运动完成 37 }}else { //全部运动都结束 38 if (PlayerPrefs.GetInt("offEffect") == 0) { //如果播放音效 39 audio.PlayOneShot(startSound); 40 } 41 logic.cue.transform.localPosition = new Vector3(0, 0, -1);//重新设置其位置 42 logic.cue.renderer.enabled = false; //设置球杆不可见 43 logic.assistBall.transform.position = new Vector3(100, 0.98f, 100); //设置球杆可见 44 logic.line.renderer.enabled = false; //设置辅助线可见 45 logic.cueBall.rigidbody.velocity = //给白球设置速度 46 new Vector3((PowerBar.restBars -1) / 22.0f * ConstOfGame.MAX_SPEED * 47 Mathf.Sin(GameLayer.totalRotation / 180.0f * Mathf.PI), 48 0, (PowerBar.restBars -1) / 22.0f * ConstOfGame.MAX_SPEED * 49 Mathf.Cos(GameLayer.totalRotation / 180.0f * Mathf.PI)); 50 GameLayer.isStartAction = false; //不允许运动 51 }}
第3行~第10行主要负责绘制提示游戏胜利失败的小界面,调用绘制游戏界面按钮方法,调用绘制小地图方法,并且根据球杆运动标志位,执行相应的球杆运动操作。
第11行~第25行为重置数据方法,包括清空两种游戏模式下的桌球列表、各种游戏对象的位置和角度、各种游戏相关物体运动的标志位,以及倒计时模式的初始时间和能量条的初始能量大小。
第29行~第36行主要负责更改游戏相关运动的标志位。每当玩家按下“GO”按钮进行击打的时候,游戏场景中分有两部分运动,第一部分为球杆向后运动,准备进行击打。第二部分运动为球杆向白球运动,进行击打。
第37行~第50行主要负责当球杆全部运动结束后的相关操作,首先播放击打白球声效,然后给白球赋予相应速度,进行击打目标球运动,同时设置球杆不可见。当所有桌球运动停止时,球杆和辅助线重新可见。
(4)编写小地图脚本。该脚本主要负责绘制游戏主界面上的小地图,是桌球游戏俯视图的缩略图。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的MiniMap.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class MiniMap : MonoBehaviour { 4 public static bool isMiniMap = true; //是否绘制小地图的标志位 5 private Texture2D[] textures; //桌球图片 6 private Texture2D miniTable; //mini球台的图片 7 private Texture2D cue; //球杆的图片 8 private float scale; //缩放比 9 private Vector2 pivotPoint; //旋转点 10 Matrix4x4 guiInvert; //获取gui的逆矩阵 11 void Start(){ 12 guiInvert = ConstOfMenu.getMatrix(); 13 scale = ConstOfGame.miniMapScale; //初始化缩放比 14 InitMiniTexture(PlayerPrefs.GetInt("billiard")); //初始化桌球图片 15 miniTable = Resources.Load("minitable") as Texture2D;//初始化mini球台的图片 16 cue = Resources.Load("cueMini") as Texture2D; //初始球杆的图片 17 } 18 public void drawMiniMap() 19 { 20 if (MiniMap.isMiniMap){ 21 GUI.DrawTexture(new Rect(0,0,283.0f/scale,153.0f/scale),miniTable); 22 for (int i = 0; i < GameLayer.BallGroup_TOTAL.Count; i++){ 23 GameObject tran = GameLayer.BallGroup_TOTAL[i] as GameObject; //获取该物体Id值 24 BallScript ballScript = tran.GetComponent("BallScript") as BallScript; //获取脚本组件 25 Vector3 ballPosition = tran.transform.position; //桌球的位置 26 int ballId = ballScript.ballId; //获取桌球的ID 27 GUI.DrawTexture(new Rect(ballPosition.z * 5 + 70, 28 ballPosition.x * 5 + 35f, 5, 5), textures[ballId]); 29 } 30 if ((GameObject.Find("Cue") as GameObject).renderer.enabled) //如果球杆可见,则绘制球杆 31 { 32 Vector3 cuePosition = (GameObject.Find("CueObject") as GameObject).transform. position; 33 Vector3 cueBallPosition=(GameObject.Find("CueBall")as GameObject).transform.position; 34 pivotPoint=new Vector2(cueBallPosition.z*5+72.5f,cueBallPosition.x*5+37f); 35 Vector3 m=guiInvert.MultiplyPoint3x4(new Vector3(pivotPoint.x,pivotPoint.y,0)); 36 GUIUtility.RotateAroundPivot(GameLayer.totalRotation, new Vector2(m.x, m.y)); 37 GUI.DrawTexture(new Rect(cuePosition.z * 5 + 45, cuePosition.x * 5 + 37f, 20, 2), cue); 38 }}} 39 void InitMiniTexture(int billiard) 40 { 41 bool init = (billiard -8) > 0; //判断模式 42 if (!init){ 43 textures = new Texture2D[16]; //如果是8球模式则初始化16张纹理图 44 for (int i = 0; i < 16; i++){ //加载纹理图 45 textures[i] = Resources.Load("minimap" + i) as Texture2D; 46 }}else{ 47 textures = new Texture2D[10]; 48 for (int i = 0; i < 10; i++){ //加载纹理图 49 textures[i] = Resources.Load("minimap" + i) as Texture2D; 50 }}}}
第4行~第10行主要负责声明相应变量数据,包括是否绘制小地图的标志位。声明缩略图里面的球桌、球杆和桌球缩略图片素材,以及缩略比和旋转点。
第11行~第17行为Start方法,主要负责初始化小地图相关数据,包括初始化缩放比、小地图桌球图片、小地图球台的图片,以及小地图球杆的图片。
第21行~第29行主要负责绘制小地图内的桌台以及桌球,而桌球的绘制需要根据玩家选择的游戏模式,以及实时桌球的运动和数量的减少。
第30行~第38行主要负责绘制球杆方法,主要根据球杆的显示与否以及球杆运动前后的位置,进行确切的绘制。
第39行~第49行主要负责初始化小地图中桌球的数量,游戏分为八球模式和九球模式,两种模式下的桌球数量不同,八球模式需要加载十六张纹理图,九球模式需要加载九张纹理图。同时,需要加载的小桌球纹理每一个也不尽相同。
(5)编写能量条脚本。该脚本主要负责绘制并实时更改母球撞击力度的能量条,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的PowerBar.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class PowerBar : MonoBehaviour 4 { 5 public static int tipIndex; 6 public Texture2D[] tipTexture; 7 public Texture2D bg; //力量滑动条背景图片 8 public Texture2D bar; //力量块整张图片 9 private int groupX = 0, groupY = 120, groupWidth = 100, groupHeight = 230, //背景图片矩形框参数 10 barX = 5, barY = 5, barW = 40, barH = 220; //力量块图片矩形参数 11 private float texX = 0, texY = 0, texW = 1, texH = 1; //纹理矩形参数 12 private int totalBars = 22; //力量块的个数 13 private int barWidth; //每个力量块的宽度 14 private Rect groupRect; //声明群组矩形变量 15 public static int restBars = 22; //定义私有变量 16 private Matrix4x4 invertMatrix; 17 Vector3 movePosition; //移动向量 18 Vector3 startPositon; //初始位置向量 19 public Texture2D[] textures; //与计时器相关的变量 20 public bool isStartTime; //初始时间 21 private int totalTime; //总时间 22 private int countTime; //计算时间 23 public static int showTime = 720; //总倒数时间 24 private int startTime; //初始化起始时间 25 private int x = 300, y = 30, numWidth = 32, numHeight = 32, span = 6; 26 private Result result; //结果类的引用 27 void Start() 28 { 29 result = GetComponent("Result") as Result; 30 if (PlayerPrefs.GetInt("billiard") == 8) { //如果是八球模式 31 tipIndex = 0; 32 }else{ 33 tipIndex = 3; 34 } 35 startTime = (int)Time.time; 36 countTime = 0; 37 totalTime = 720; //总时间为720秒 38 isStartTime = PlayerPrefs.GetInt("isTime") > 0;//是否为倒计时模式的标志位 39 startPositon = Vector3.zero; 40 movePosition = Vector3.zero; 41 invertMatrix = ConstOfMenu.getInvertMatrix(); 42 groupRect = new Rect(groupX, groupY, groupWidth, groupHeight + 100); //初始化变量 43 barWidth = barH / totalBars; //计算每个力量块的宽度 44 } 45 ……//此处省略了Update方法,将在下面进行介绍 46 ……//此处省略了DrawTime方法,将在下面进行介绍 47 ……//此处省略了OnGUI方法,将在下面进行介绍 48 }
第5行~第15行声明了力量条的相关变量,包括整个能量条的背景纹理图和色彩纹理图,以及能量条和每个能量块的宽度、高度等。由于在该游戏中,将能量条均匀分成了多个能量块,固能按比例转化为白球击球的能量。
第17行~第26行主要负责游戏进行过程中计时变量的声明,其中包括倒计时模式的总时间,从零开始的计时时间,和倒计时模式的剩余时间等。
第28行~第43行为Start方法,主要负责初始化声明的相关变量,定义倒计时模式总时间为720秒,即12分钟,也要对能量块进行绘制。
(6)下面介绍能量条脚本中省略的Update方法和DrawTime方法。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的PowerBar.cs。
1 void Update(){ 2 if (isStartTime) { //如果为倒计时模式 3 countTime = (int)Time.time - startTime; 4 showTime = totalTime - countTime; //显示的时间为总时间与计时的差 5 if (showTime<=0) { //如果时间小于0则表示游戏失败 6 result.goLoseScene();//调用result组件的goLoseScene方法进入到失败界面 7 }} 8 if (GameLayer.TOTAL_FLAG){ 9 if (Input.GetMouseButtonDown(0)){ 10 CamControl.prePosition = Input.mousePosition; 11 CamControl.touchFlag = false; 12 startPositon = invertMatrix.MultiplyPoint3x4(Input.mousePosition); 13 movePosition = startPositon; 14 } 15 if (Input.GetMouseButton(0)) { //当触摸单击屏幕或在屏幕上滑动时 16 movePosition = //得到触摸点位置,井进行自适应 17 invertMatrix.MultiplyPoint3x4(Input.mousePosition); 18 } 19 if (Input.GetMouseButtonUp(0)){ //当触摸结束 20 CamControl.touchFlag = false; //触摸标志位置为false 21 }}} 22 void DrawTime(int time) //绘制时间方法 23 { 24 int minute = time / 60; //定义分,取整 25 int seconds = time % 60; //定义秒,取余 26 int num1 = minute / 10; //定义num1,取整 27 int num2 = minute % 10; //定义num2,取余 28 int num3 = seconds / 10; //定义num3,取整 29 int num4 = seconds % 10; //定义num4,取余 30 GUI.BeginGroup(new Rect(x, y, 5 * (numWidth + span), numHeight));//绘制分钟纹理图 31 GUI.DrawTexture(new Rect(0, 0, numWidth, numHeight), textures[num1]); 32 GUI.DrawTexture(new Rect((numWidth+span),0,numWidth,numHeight),textures[num2]); 33 GUI.DrawTexture(new Rect(2 * (numWidth + span), 0, 34 numWidth, numHeight), textures[textures.Length -1]); //绘制秒钟纹理图 35 GUI.DrawTexture(new Rect(3 * (numWidth + span), 0, numWidth, numHeight), textures[num3]); 36 GUI.DrawTexture(new Rect(4 * (numWidth + span), 0, numWidth, numHeight), textures[num4]); 37 GUI.EndGroup(); 38 }
第2行~第7行主要负责倒计时模式下的计时功能。随着游戏开始,时间进行缩减。如果时间小于0则表示游戏失败,会调用Result组件的goLoseScene方法进入到失败界面。
第8行~第20行主要负责能量条的实时更改。在该游戏中,用户可以通过抹动进行能量条的调整,或者直接单击需要的能量大小,便根据用户需要,更改能量条能量。
第22行~第37行主要负责绘制时间方法,总时间为12分钟,秒数精确到第二位。每一个时间数字都是固定的纹理图,根据时间的更改,更换纹理图的绘制。
(7)下面介绍能量条脚本中省略的OnGUI方法。该方法主要负责能量条的具体绘制,以及当用户单击能量条的时候,对能量块的绘制进行相应更改。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的PowerBar.cs。
1 void OnGUI() 2 { 3 GUI.matrix = ConstOfMenu.getMatrix(); 4 GUI.DrawTexture(new Rect(272, 5, 256, 16), tipTexture[tipIndex]);//绘制纹理图 5 if (isStartTime){ 6 DrawTime(showTime); //显示时间 7 } 8 GUI.BeginGroup(groupRect); //开始群组 9 GUI.DrawTexture(new Rect(0, 0, groupWidth, groupHeight), bg); //绘制力量条背景 10 GUI.DrawTextureWithTexCoords(new Rect(barX, barY + 11 barWidth * (totalBars - restBars), barW, barWidth * restBars), bar, 12 new Rect(texX, texY, texW, texH * restBars / totalBars)); //绘制力量块 13 GUI.EndGroup(); //结束群组 14 if (new Rect(barX + groupX, barY + groupY, barW, 15 barH).Contains(new Vector2(startPositon.x,480.0f-startPositon.y))){ 16 CamControl.touchFlag = false; //如果鼠标力量条区域 17 restBars = Mathf.Clamp(totalBars - //如果鼠标位于力量条矩形内 18 (int)(480.0f - movePosition.y - barY - groupY) / 19 barWidth, 1, 22); //计算需要绘制的力量块个数 20 }else{ 21 if (new Rect(0, 420, 800, 60).Contains(new Vector2( 22 movePosition.x, 480.0f - movePosition.y))){ 23 if (new Rect(0, 420, 800, 60).Contains(new Vector2( 24 startPositon.x, 480.0f - startPositon.y))){ 25 CamControl.touchFlag = false; //如果鼠标按钮区域 26 }}else if (new Rect(730, 10, 30, 30).Contains(new Vector2( 27 movePosition.x, 480.0f - movePosition.y))){ 28 if (new Rect(730, 10, 30, 30).Contains(new Vector2( 29 startPositon.x, 480.0f - startPositon.y))){ 30 CamControl.touchFlag = false; //如果鼠标按钮区域 31 }}else{ 32 ontrol.touchFlag = true; 33 }}}}
第3行~第13行为基本绘制方法,主要负责绘制能量条的透明背景图,即能量百分比显示图。同时绘制能量块纹理图。
第14行~第19行主要负责能量条绘制的更改方法,当用户对能量条进行单击的时候,需要计算用户单击的能量块,重新绘制,同时更改能量大小。
第20行~第32行主要负责当用户在能量条上抹动的时候,能量条需要跟随触摸点的移动,进行实时绘制,与此同时能量条不允许单击。
(8)下面介绍能量条脚本的变量赋值。在该脚本中包括能量条纹理图、能量块纹理图,以及需要绘制的时间纹理图,详细如图2-63所示。变量赋值在前面章节已经详细讲过,这里不再赘述。
▲图2-63 能量条脚本变量赋值
2.5.4 功能脚本的编写
上一小节已经介绍了界面相关脚本的编写,本小节将向读者介绍关于游戏功能的相关脚本。每个脚本负责不同的功能,详细的编写和挂载步骤介绍如下。
(1)编写进球检测脚本。该脚本挂载在“CubeB”球洞面板游戏对象下,主要为桌球进洞的回调方法,同时检测桌球进洞后播放音效。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的Cube.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class Cube : MonoBehaviour { 4 public AudioClip BallinEffect; //进球的音效 5 void OnCollisionEnter(Collision other) { //刚体碰撞时的回调方法 6 if (other.gameObject.tag == "Balls"){//如果球和此刚体碰撞,则表明球已经进洞 7 if (PlayerPrefs.GetInt("offEffect") == 0) { //如果播放音效 8 audio.PlayOneShot(BallinEffect); //播放音效 9 } 10 (other.gameObject.GetComponent("BallScript")as BallScript).isAlowRemove=true; 11 } //设置球删除的标志位为true 12 } 13 }
第4行~第9行声明了桌球进洞的音效,同时介绍了桌球与该游戏对象碰撞时候的回调方法。如果桌球和此刚体碰撞,则表明球已经进入洞中,此时要播放桌球进洞音效。
第10行为将进洞的桌球设置其删除的标志位为true。游戏对象的桌球个数根据玩家选择的游戏种类分为两种,要分别记载未进洞和进洞的桌球个数,进洞的桌球需要删除。
(2)编写桌球脚本。该脚本挂载在“Ball”和“CueBall”游戏对象下,主要负责控制桌球碰撞的声音以及桌球碰撞效果,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的BallScript.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class BallScript : MonoBehaviour { 4 public bool isAlowRemove = false; //是否删除的标志位 5 public int ballId = 0; //桌球ID 6 public AudioClip BallHit; //桌球互相碰撞发出的声音 7 public bool setData = true; //桌球数据标志位 8 void Update() 9 { 10 this.transform.rigidbody.velocity = this.transform.rigidbody.velocity * 0.988f; 11 this.transform.rigidbody.angularVelocity = this.transform.rigidbody.angularVelocity * 0.988f; 12 if (this.transform.rigidbody.velocity.sqrMagnitude < 0.01f) { //设置速度阈值 13 this.transform.rigidbody.velocity = Vector3.zero; } 14 if (Mathf.Abs(this.transform.position.z) > 12.3f || Mathf.Abs(this.transform. position.x) > 6.1f) 15 { 16 if (setData) { 17 collider.material.bounciness = 0.2f; //设置弹性系数 18 this.transform.rigidbody.velocity = this.transform.rigidbody. velocity / 4; //重新设置其速度 19 setData = false; //重新设置数据的标志位为false 20 }}else{ 21 if (Mathf.Abs(this.transform.rigidbody.velocity.y) >= 3f) { //防止桌球蹦起来 22 this.transform.rigidbody.velocity = Vector3.zero; } 23 collider.material.bounciness = 1; //重新设置弹性系数 24 setData = true; //重新设置重置数据标志位 25 }} 26 void OnCollisionEnter(Collision collision) 27 { 28 if (PlayerPrefs.GetInt("offEffect") == 0){ //如果播放音效 29 if (collision.gameObject.tag == "Balls"){ //如图是球和球之间的碰撞 30 float speedOfMySelf = gameObject.rigidbody.velocity.magnitude; //比较两个球的速度大小 31 float speedOfAnother = collision.rigidbody.velocity.magnitude; 32 if (speedOfMySelf > speedOfAnother) { //速度大的播放音效 33 audio.volume = speedOfMySelf / ConstOfGame.MAX_SPEED; 34 audio.PlayOneShot(BallHit); //播放碰撞音效 35 }}}}}
第4行~第7行声明了桌球是否需要删除的标志位,如果桌球进洞,需要在桌球系统中删除该桌球的号码;声明了桌球ID,方便管理桌球系统;声明桌球碰撞的声音;声明了桌球数据标志位。
第10行~第13行主要为桌球赋予一定的速度控制。为了模拟现实状况,桌球的速度需要有一定的衰减。完成运动后,桌球的速度需要自动归零。
第14行~第24行主要负责控制桌球的速度。为了模拟现实中桌球的运动碰撞状态,需要为其设置相应的弹性系数。同时为了防止桌球在球桌上弹跳起来,需要控制其速度。
第26行~第34行主要负责桌球之间碰撞的时候,播放音效。首先要比较两个桌球速度的大小,速度较大的球播放音效。
(3)桌球脚本的变量赋值。由于桌球在碰撞过程中会产生撞击声音,所以为其添加碰撞音效,将文件夹Assets/Sounds中的声音资源拖到桌球脚本下,详细如图2-64所示。
▲图2-64 桌球脚本变量赋值
(4)编写桌球阴影脚本。该脚本挂载在“Shadow”桌球阴影游戏对象下,主要为桌球绘制实时阴影。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的Shadow.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class Shadow : MonoBehaviour { 4 Vector3 parentPosition; //桌球的位置变量 5 void Update () { 6 parentPosition = transform.parent.position; //获取其父类位置 7 transform.rotation = new Quaternion(1,0,0,-Mathf.PI*0.32f); //设置阴影屏幕的旋转角度 8 transform.position=new Vector3(parentPosition.x,0.55f,parentPosition. z -0.4f); 9 } //根据桌球的位置,设置阴影的位置 10 }
说明
该脚本主要负责绘制桌球阴影,首先获取父类桌球位置,然后在此位置绘制桌球实时阴影。同时,桌球阴影需要根据玩家视角实时更新。
(5)编写计算辅助线脚本。该脚本挂载在“AssistBall”游戏对象下,主要负责绘制母球与目标球之间的辅助线。具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CalculateLine.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class CalculateLine : MonoBehaviour{ 4 public GameObject line; //声明辅助线对象 5 public GameObject cueBall; //声明母球对象 6 public GameObject allScript; //声明图片资源 7 InitAllBalls initAllBalls; //声明initAllBalls脚本组件 8 CamControl camControl; //声明camControl脚本组件 9 public ParticleSystem particle; //声明粒子系统 10 public Color c = Color.green; //声明桌球闪烁颜色,默认为绿色 11 private float alpha = 1; 12 private int mode; //得到模式的整数形式 13 void Start(){ 14 mode = PlayerPrefs.GetInt("billiard"); 15 initAllBalls = allScript.GetComponent("InitAllBalls") as InitAllBalls; //获取脚本组件 16 camControl = allScript.GetComponent("CamControl") as CamControl; //获取脚本组件 17 } 18 void Update() 19 { 20 if (GameLayer.TOTAL_FLAG) 21 { 22 GameObject tableBall_N = calculateBall();//计算距离母球辅助线最近的球 23 calculateUtil(tableBall_N); //计算辅助线的位置,长度,以及辅助球的位置等 24 ParticalBlint(tableBall_N); //粒子闪烁的方法 25 }} 26 Vector3 HitPoint() 27 { 28 Vector3 point = Vector3.zero; //声明碰撞点位置 29 RaycastHit hit; 30 if(Physics.Raycast(cueBall.transform.position,line.transform.forward,o ut hit,100)) 31 { 32 if (hit.transform.tag == "table"){ 33 point = hit.point; 34 }} 35 return point; 36 } 37 void RedColor(){ //球进行红色闪烁 38 c = Color.Lerp(Color.red, Color.red/2, Mathf.PingPong(Time.time, 1)); 39 } 40 void GreenColor(){ //球进行绿色闪烁 41 c=Color.Lerp(Color.green,Color.green/2,Mathf.PingPong(Time.time,1)); 42 } 43 void FullColor(){ //球进行彩色闪烁 44 c = Color.Lerp(Color.yellow, Color.blue, Mathf.PingPong(Time.time, 1)); 45 } 46 ……//此处省略了calculateBall方法,将在下面进行介绍 47 ……//此处省略了calculateUtil方法,将在下面进行介绍 48 ……//此处省略了ParticalBlint方法,将在下面进行介绍 49 }
第4行~第12行主要负责声明相关内容,其中包括辅助线对象、母球对象、粒子系统和所用的图片资源等。同时声明了initAllBalls脚本组件和camControl脚本组件。
第13行~第17行主要为“Start”方法,在此获取了两个文本组件。获取initAllBalls组件的目的主要是为了使用其已经加载好的纹理图。获取CamControl组件主要负责统一桌球行为和性质。
第18行~第25行主要为“Update”方法,主要负责计算距离母球辅助线最近的球,计算辅助线的位置、长度以及辅助球的位置等。同时调用粒子闪烁的方法,该方法将在后面进行详细介绍。
第26行~第36行主要负责进行碰撞点计算。游戏对象附加的table层用于进行碰撞检测,碰撞到的点用于计算线的位置以及线的长度。
第37行~第45行为桌球闪烁的三种颜色的闪烁方法,其中包括红色、绿色和彩色闪烁。
(6)下面开始介绍上述计算辅助线脚本中省略的“calculateBall”方法。该方法用于寻找距离白球最近的桌球,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CalculateLine.cs。
1 GameObject calculateBall() 2 { 3 GameObject tableBall_N = null; //初始化一个球对象 5 if (Mathf.Abs(GameLayer.totalRotation) == 90) //判断斜率是否存在 6 { 7 return null; 8 } 9 Vector2 position0 = new Vector2( //获取白球的位置,转换到2D平面 10 cueBall.transform.position.z,-cueBall.transform.position.x); 11 Vector2 forceVector = new Vector2(Mathf.Cos(-GameLayer.totalRotation //计算方向向量 12 / 180.0f * Mathf.PI), Mathf.Sin(-GameLayer.totalRotation / 180.0f * Mathf.PI)); 13 float k = forceVector.y / forceVector.x; //计算斜率 14 for (int i = 1; i < GameLayer.BallGroup_TOTAL.Count; i++) 15 { 16 GameObject tableBall_M = 17 GameLayer.BallGroup_TOTAL[i] as GameObject; //获取球的位置 18 BallScript ballScript = 19 tableBall_M.GetComponent("BallScript") as BallScript;//获取脚本组件 20 Vector2 position_M = new Vector2( 21 tableBall_M.transform.position.z, -tableBall_M.transform.position. x); //计算两个点 22 Vector2 vectorM_0 = new Vector2( 23 position_M.x - position0.x, position_M.y - position0.y); 24 float length = Mathf.Abs(position_M.y - k * position_M.x - position0.y+ //计算距离 25 position0.x * k) / Mathf.Sqrt(1 + k * k); 26 if (length <= 1 && Vector2.Angle(vectorM_0, forceVector) < 27 Mathf.Acos(1 / 2) * Mathf.Rad2Deg) 28 { 29 if(tableBall_N) //若tableBall_N存在 30 { 31 Vector2 position_A = 32 new Vector2(tableBall_N.transform.position.z, //找到球的位置 33 -tableBall_N.transform.position.x); 34 Vector2 position_B = position_M; //待判定球的位置 35 float length1 = //计算lenght1 36 Vector2.SqrMagnitude(new Vector2( 37 position_A.x - position0.x,position_A.y - position0.y)); 38 float length2 = //计算lenght2 39 Vector2.SqrMagnitude(new Vector2( 40 position_B.x - position0.x,position_B.y - position0.y)); 41 if (length1 > length2){ //若length1大于length2 42 tableBall_N = tableBall_M; 43 }}else{ 44 tableBall_N = tableBall_M;//如果tableBall_N不存在,则直接赋值 45 }}} 46 return tableBall_N; //返回计算出的球体 47 }
第3行~第13行主要负责初始化一个桌球对象,判断斜率,获取白球的位置并转换到2D平面,最后计算辅助线方向向量。
第14行~第25行主要负责循环桌球列表,寻找距离辅助线最近的球。首先获取球的位置,然后获取BallScript脚本组件。最后计算白球与目标球的两个点,从而计算出两球的距离。
第26行~第46行为负责判定与白球距离最近的球。其中包括比较规则,要求与母球距离更近的球为tableBall_N。
(7)下面开始介绍上述计算辅助线脚本中省略的“calculateUtil”方法。该方法用于计算白球与对应桌球的碰撞点,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CalculateLine.cs。
1 void calculateUtil(GameObject tableBall_N ) //计算球的碰撞点的方法 2 { 3 Vector2 forceVector = //计算方向向量 4 new Vector2(Mathf.Cos(-GameLayer.totalRotation / 180.0f * Mathf.PI), 5 Mathf.Sin(-GameLayer.totalRotation / 180.0f * Mathf.PI)); 6 if (tableBall_N) 7 { 8 BallScript ballScript = 9 tableBall_N.GetComponent("BallScript") as BallScript; //获取脚本组件 10 transform.LookAt(camControl.cameras[CamControl.curCam].transform.position); 11 float k = forceVector.y / forceVector.x; //计算斜率 12 Vector2 position_N = new Vector2( //获取位置 13 tableBall_N.transform.position.z, -tableBall_N.transform.position.x); 14 Vector2 position0 = new Vector2( //白球的位置 15 cueBall.transform.position.z,-cueBall.transform.position.x); 16 Vector2 vector0_N = new Vector2( //计算两个球的连线向量 17 position_N.x - position0.x,position_N.y - position0.y); 18 float length1 = Mathf.Abs(position_N.y - k * position_N.x - position0.y 19 + position0.x * k) / Mathf.Sqrt(1 + k * k); //计算球与直线的距离 20 float length2 = Vector2.SqrMagnitude(vector0_N);//计算两个球之间的距离的平方 21 float length3 = Mathf.Sqrt(1- length1 * length1); //计算距离 22 float length4 = Mathf.Sqrt(length2- length1 * length1) - length3; //计算距离 23 Vector2 point1 = forceVector * length4; 24 Vector2 point2 = position0 + point1; 25 transform.position = new Vector3(-point2.y, 0.98f, point2.x);//变换位置 26 Vector2 point3 = forceVector * length4; //线的长度 27 Vector2 point4 = position0 + point3 / 2; //线的位置 28 line.transform.position = 29 new Vector3(-point4.y, 0.98f, point4.x); //设置辅助线的位置 30 line.transform.localScale = 31 new Vector3(0.005f, 1, (length4-1f) / 10.0f);//设置缩放比 32 line.renderer.material.mainTextureOffset = 33 new Vector2(0, Time.time * 0.03f); //设置图片偏移量 34 line.renderer.material.mainTextureScale = 35 new Vector2(1, (length4-1) / 12); //设置缩放比 36 }else{ 37 Vector3 hitPoint3 = HitPoint(); 38 Vector2 hitPoint = new Vector2(hitPoint3.z, -hitPoint3.x); 39 Vector2 position0 = new Vector2( //白球的位置 40 cueBall.transform.position.z,-cueBall.transform.position.x); 41 Vector2 vector0_N = new Vector2( 42 hitPoint.x - position0.x, hitPoint.y - position0.y); //计算两个球的连线向量 43 Vector2 point1 = (position0 + hitPoint) / 2 + 0.5f * forceVector; 44 transform.position = new Vector3(100, 0.98f,100); //把辅助球移动出屏幕 45 float length1 = Vector2.Distance(Vector2.zero,vector0_N); 46 line.transform.position = new Vector3(-point1.y, 0.98f, point1.x); //设置辅助线的位置 47 line.transform.localScale = 48 new Vector3(0.005f, 1, length1 / 10.0f); //设置缩放比 49 line.renderer.material.mainTextureOffset = 50 new Vector2(0, Time.time * 0.03f); //设置图片偏移量 51 line.renderer.material.mainTextureScale = 52 new Vector2(1, length1 / 12); //设置缩放比 53 }}
第3行~第5行主要负责计算方向向量,由于该方法需要计算两个球的碰撞点,所以首先需要通过相关数学公式计算出方向向量,以便后续使用。
第8行~第17行为当tableBall_N存在的时候,首先获取“BallScript”脚本组件,为辅助球赋予相应的桌球性质特点。然后获取发生碰撞的白球和另一个桌球的位置,计算其斜率,并计算两个球的连线向量,方便后续使用。
第18行~第36行主要负责计算球与直线的距离,计算两个球之间距离的平方,从而计算出两球之间的距离。通过相应的变换位置能确定辅助线的长度,从而设置辅助线位置和偏移量。由于游戏需要适应各种移动终端设备,需要进行缩放比设置。
第37行~第43行为当tableBall_N不存在的时候,首先确定两个球的位置,从而计算两个球的连线方向向量。
第44行~第52行主要负责设置白球到目标球的辅助线的位置和偏移量。由于游戏需要适应各种移动终端设备,需要进行缩放比设置。
(8)下面开始介绍上述计算辅助线脚本中省略的“ParticalBlint”方法。该方法用于控制桌球闪烁的颜色。当白球想要击打一个桌球的时候,辅助球根据游戏规则会闪烁不同的颜色。红色表示警告不可击打,绿色表示可以击打,彩色表示该球进洞即为胜利,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的CalculateLine.cs。
1 void ParticalBlint(GameObject tableBall_N) 2 { 3 if (tableBall_N){ 4 BallScript ballScript = 5 tableBall_N.GetComponent("BallScript") as BallScript; //获取脚本组 件 6 transform.renderer.material.mainTexture = 7 initAllBalls.textures[ballScript.ballId]; //设置辅助球的材质 8 int num = ConstOfGame.kitBallNum; //设置辅助球的闪烁颜色 9 if (mode < 9){ //如果是8球模式 10 if (num == 0) { //表示可击打任意球 11 if (ballScript.ballId == 8) { 12 RedColor(); //红色警告不可击打 13 }else{ 14 GreenColor(); //绿色闪烁,可以击打 15 }}else if (num == 1){ //击打全色球 16 if (ballScript.ballId > 8) { 17 RedColor(); //红色闪烁表示警告不可击打 18 }else if (ballScript.ballId == 8){ 19 int one_count = GameLayer.BallGroup_ONE_EIGHT.Count; 20 int two_count = GameLayer.BallGroup_TWO_EIGHT.Count; 21 if (one_count == 0 || two_count == 0) { 22 FullColor(); //彩色闪烁 23 }else{ 24 RedColor(); //红色闪烁表示警告不可击打 25 }}else{ //击打全色球 26 GreenColor(); //绿色闪烁,可以击打 27 }}else{ //击打花色球 28 if (ballScript.ballId > 8) { 29 GreenColor(); //绿色闪烁,可以击打 30 }else if (ballScript.ballId == 8){ 31 int one_count=GameLayer.BallGroup_ONE_EIGHT.Count; 32 int two_count=GameLayer.BallGroup_TWO_EIGHT.Count; 33 if (one_count == 0 || two_count == 0){ 34 FullColor(); //表示同一种球全部进洞,彩色闪烁 35 }else{ 36 RedColor(); //红色闪烁表示警告不可击打 37 }}else{ //击打全色球 38 RedColor(); //红色闪烁表示警告不可击打 39 }}}else { //如果是9球模式即共有9个球 40 if (ballScript.ballId == 8) { 41 int one_nine = GameLayer.BallGroup_ONE_NINE.Count; 42 if (one_nine == 0){ 43 FullColor(); //表示可击打黑色八号球进洞,彩色闪烁 44 }else{ 45 RedColor(); //红色闪烁,表示不可击打 46 }}else{ 47 GreenColor(); //可击打任意球,绿色闪烁 48 }} 49 alpha = Mathf.Lerp(0.5f, 1, Mathf.PingPong(Time.time, 1)); 50 particle.startColor = c; 51 }}
第4行~第8行主要负责设置辅助球的材质和辅助球的闪烁颜色,同时获取BallScript脚本组件。
第9行~第38行主要负责八球模式桌球闪烁颜色,即共有十五个球。其中,第一个球可以击打任意一个,若第一个入洞的球为花色,则下一个必需为全色球,绿色闪烁。若不按规则击打则红色闪烁。当所有球入洞后,最后击打黑色八号球,闪烁彩色,表示该球入洞后即可胜利。
第39行~第50行主要负责九球模式,即共有九个球。所有球可任意击打,绿色闪烁。但若离白球最近的为黑色八号球,闪烁红色表示不可击打。最后击打黑色八号球,闪烁彩色,表示该球入洞后即可胜利。
(9)计算辅助线脚本的挂载和变量赋值。将“CalculateLine”脚本拖曳到“AssistBall”游戏对象下。详细如图2-65所示。
▲图2-65 辅助线脚本的挂载和变量赋值
(10)初始化桌球脚本。在前面计算辅助线脚本中,加载了该脚本,在这里进行详细介绍。该脚本主要负责根据不同游戏模式,初始化相应数目的桌球,详细代码如下。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的initAllBalls.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class InitAllBalls : MonoBehaviour { 4 public GameObject ball; //预设对象 5 public Texture2D[] textures; //桌球图片 6 public void initAllBalls(int billiard) { //初始化桌球的方法 7 int[] randomArray = RandomArray(billiard,7); 8 bool init = (billiard -8) > 0; //判断模式,大于0为9球模式 9 int sum = 0; 10 if (!init){ 11 textures = new Texture2D[16]; //如果是8球模式则初始化16张纹理图 12 for (int i = 0; i < 16; i++){ 13 textures[i] = Resources.Load("snooker" + i) as Texture2D; //加载纹理图 14 } 15 for (int i = 1; i <= 5; i++){ 16 for (int j = 1; j <= i; j++){ 17 Vector3 ballPosition = new Vector3(-(0.5f + 0.05f) * 18 (i -1) + (j -1) * (0.5f + 0.05f) * 2, 0.98f, 5.8f + 19 (0.5f + 0.05f) * 2 * (i -1)); //计算桌球的位置 20 GameObject obj = Instantiate(ball, ballPosition, 21 new Quaternion(1,0,0,Mathf.PI/2)) as GameObject; //实例化桌球 22 obj.transform.renderer.material.mainTexture = 23 textures[randomArray[sum + j -1] + 1];//设置桌球图片 24 (obj.GetComponent("BallScript") as BallScript).ballId = 25 randomArray[sum + j -1] + 1; //设置桌球的ID号码 26 if ((randomArray[sum + j -1] + 1) < 8) 27 { //将1-7号球添加到八球模式下的1号列表 28 GameLayer.BallGroup_ONE_EIGHT.Add(obj); 29 }else if ((randomArray[sum + j -1] + 1) > 8) 30 { //将9-15号球添加到八球模式下的2号列表 31 GameLayer.BallGroup_TWO_EIGHT.Add(obj); 32 } 33 GameLayer.BallGroup_TOTAL.Add(obj); 34 } 35 sum += i; 36 }}else{ 37 ……//此处省略了9球模式的桌球初始化代码,与8球模式类似,不再赘述 38 }} 39 ……//此处省略了RandomArray方法,将在下面进行介绍 40 }
第4行~第14行声明了桌球预设对象和桌球的纹理图片。同时需要判断用户选择何种游戏模式,游戏模式分为八球模式和九球模式,根据不同的模式,加载不同数目和样式的桌球纹理图。
第15行~第25行计算每一个桌球的位置,并实例化桌球,同时设置各个桌球的图片。并为每个桌球设置固定的ID号码,方便后续使用。
第26行~第32行表示将八号球的两种花色分别添加到各自的列表,由于八球模式分为全色球和花色球,根据八球模式游戏的规则,必须交叉入洞,所以要为两种桌球分类管理。
(11)下面开始介绍上述初始化桌球脚本中省略的“RandomArray”方法。该方法用于为八球模式和九球模式的桌球分别设置相应的ID号码,具体代码如下所示。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的initAllBalls.cs。
1 private int[] RandomArray(int length, int index) 2 { 3 length = length > 8 ? 9 : 15; //生成特定随机数序列的数组 4 ArrayList origin = new ArrayList(); //实例化一个ArrayList 5 int[] result = new int[length]; //实例化一个长度为length的数组,最后返回该数组 6 for (int i = 0; i < length; i++){ //遍历列表并初始化 7 if (i == index) { //当遍历到第index个元素时 8 continue; //不将index元素放入列表 9 } 10 origin.Add(i); //将i元素加入列表中 11 } 12 for (int i = 0; i < length; i++) { //遍历数组 13 if (i == 4) { //当遍历到第index个元素时 14 result[i] = index; //为第index个元素赋固定的值 15 continue; //继续下一次遍历 16 } 17 int tempIndex = (int)Random.Range(0, origin.Count -0.1f); //产生随机位置 18 result[i] = (int)origin[tempIndex];//将从列表中随机位置上取出的元素赋值给数组 19 origin.RemoveAt(tempIndex); //从列表中删除对应的取出的元素 20 } 21 return result; //返回结果 22 }
第3行~第11行主要负责判断传入参数为八球模式还是九球模式,根据判断结果进行相应的实例化数组操作,同时遍历列表并初始化。 第12行~第21行遍历数组,当遍历到第index个元素时,为该元素赋固定的值。同时产生随机位置,将从列表中随机位置上取出的元素赋值给数组,并从列表中删除对应的取出的元素。最后返回操作结果。
(12)编写游戏结果脚本。该脚本主要负责根据游戏进行的情况,判断胜利与失败,同时在游戏界面上显示相应的提示小界面,其详细代码如下。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的Result.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class Result : MonoBehaviour { 4 private bool isResult; //是否已经产生结果的标志位 5 public Texture2D backGround; //背景图片 6 public Texture2D dialog; //中心背景图片 7 public Texture2D[] tipTexture; //提示信息图片 8 private int tipIndex; //提示信息索引 9 Matrix4x4 guiMatrix; //GUI自适应矩阵 10 public GUIStyle[] guiSytle; //按钮样式 11 Logic logic; //获取logic脚步组件 12 PowerBar powerBar; //获取PowerBar脚步组件 13 void Awake() 14 { 15 logic = GetComponent("Logic") as Logic; //获取主控逻辑脚本组件 16 powerBar = GetComponent("PowerBar") as PowerBar; //获取能量条脚本组件 17 tipIndex = 0; //定义相应变量 18 guiMatrix = ConstOfMenu.getMatrix(); 19 isResult = false; //结果标志位置为false 20 } 21 void OnGUI() 22 { 23 if (isResult){ 24 GameLayer.TOTAL_FLAG = false; 25 GUI.matrix = guiMatrix; 26 GUI.DrawTexture(new Rect(0, 0, 800, 480), backGround); //绘制灰色的背景图片 27 GUI.BeginGroup(new Rect(200,150,400,180)); //设定组 28 GUI.DrawTexture(new Rect(0, 0, 400, 180), dialog); //绘制背景 29 GUI.DrawTexture(new Rect(100, 20, 200,50), tipTexture[tipIndex]); //绘制提示信息 30 if (GUI.Button(new Rect(30,100,150,50), "", guiSytle[0])) { 31 GameLayer.resetAllStaticData(); //如果重新开始,则重新进行常量的设置, 32 Application.LoadLevel("GameScene"); //重新加载该场景 33 } 34 if (GUI.Button(new Rect(220, 100, 150, 50), "", guiSytle[1])){ 35 Application.Quit(); //按下退出游戏,游戏退出 36 } 37 GUI.EndGroup(); 38 }} 39 public static string[] LoadData() //加载数据方法 40 { 41 string[] records = PlayerPrefs.GetString("gameData").Split(';'); //游戏记录 42 return records; //返回结果 43 } 44 ……//此处省略了goVectorScene方法,将在下面进行介绍 45 ……//此处省略了goLoseScene方法,将在下面进行介绍 46 ……//此处省略了SaveData方法,将在下面进行介绍 47 }
第4行~第12行声明了该游戏是否结束的标志位、按钮样式,以及游戏结束后提示的小界面的背景图,覆盖在游戏界面上的纹理图,以及提示再玩一次和退出游戏两个按钮的纹理图。
第13行~第20行主要负责获取主控逻辑脚本组件和能量条脚本组件,同时对相应变量进行初始化。
第23行~第38行主要负责,当游戏结束标志位为true的时候,对结束提示界面进行绘制,同时将游戏背景用灰度图覆盖,当用户单击再玩一次的时候,游戏重新开始,所有变量恢复初始化状态。当用户单击退出游戏的时候,游戏便退出。
第39行~第43行主要为加载数据的方法,在该方法中,对游戏时间记录进行了获取,同时返回结果。
(13)下面介绍上述游戏结果脚本省略的goVectorScene方法、goLoseScene方法和SaveData方法。前两者为游戏胜利和失败界面的方法,后者为保存数据方法,保存的数据用于显示在排行榜中。详细代码如下。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的Result.cs。
1 public void goVectorScene() //游戏胜利的方法 2 { 3 powerBar.isStartTime = false; //标志位置为false 4 logic.enabled = false; 5 for (int i = 0; i < GameLayer.BallGroup_TOTAL.Count; i++) 6 { //循环遍历需要列表,将球的速度设置为0 7 GameObject ball = GameLayer.BallGroup_TOTAL[i] as GameObject; 8 ball.transform.rigidbody.velocity = Vector3.zero; 9 ball.transform.rigidbody.angularVelocity = Vector3.zero; 10 } 11 isResult = true; 12 tipIndex = 1; 13 if (PowerBar.showTime != 720){ 14 SaveData(PowerBar.showTime); //数据存储,到时候把代码粘过来 15 }} 16 public void goLoseScene() //游戏失败的方法 17 { 18 powerBar.isStartTime = false; 19 logic.enabled = false; 20 for (int i = 0; i < GameLayer.BallGroup_TOTAL.Count; i++) 21 { //循环遍历需要列表,将球的速度设置为0 22 GameObject ball = GameLayer.BallGroup_TOTAL[i] as GameObject; 23 ball.transform.rigidbody.velocity = Vector3.zero; 24 ball.transform.rigidbody.angularVelocity = Vector3.zero; 25 } 26 isResult = true; 27 tipIndex = 0; 28 } 29 public static void SaveData(int score) //数据保存方法 30 { 31 int year = System.DateTime.Now.Year; //获取年份 32 int month = System.DateTime.Now.Month; //获取月份 33 int day = System.DateTime.Now.Day; //获取日子 34 string date = year + "-" + month + "-" + day; //按照日期格式进行重组 35 string oldData = PlayerPrefs.GetString("gameData"); 36 string gameData = ""; //定义游戏数据存储变量 37 if (oldData == ""){ //当存储为空时 38 gameData = date + "," + score; //为游戏数据存储变量赋值 39 }else{ 40 gameData = oldData + ";" + date + "," + score; //为游戏数据存储变量赋值 41 } 42 PlayerPrefs.SetString("gameData", gameData); //保存游戏数据 43 }
第1行~第15行主要为游戏胜利的方法,当游戏胜利之后,将所有桌球速度置零,同时记录下游戏胜利的时间,用于显示在排行榜上。
第16行~第28行主要为游戏失败的方法,将所有桌球速度置零,因为游戏失败则无需做任何记录。
第29行~第43行主要为数据保存方法,该方法主要负责保存游戏胜利的数据,包括游戏的年份、月份和日期,同时整理为“年—月—日”格式。
(14)下面介绍游戏结果脚本的变量赋值。包括游戏结束后提示的小界面的背景图,覆盖在游戏界面上的纹理图,以及提示再玩一次和退出游戏两个按钮的纹理图,详细如图2-66所示。变量赋值在前面章节已经详细讲过,不再赘述。
▲图2-66 能量条脚本变量赋值
(15)主控逻辑脚本的编写。该脚本主要负责游戏中各个游戏对象的正常运转、功能的正常执行,以及遵循游戏规则进行判断输赢。其详细代码如下。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的Logic.cs。
1 using UnityEngine; 2 using System.Collections; 3 public class Logic : MonoBehaviour { 4 private bool isEightMode; //模式的标志位 5 private bool isJudgeOver; //第一次击球是否结束的标志位 6 public bool resetPositionFlag; //白球是否重置位置的标志位 7 public GameObject cue; //球杆对象 8 public GameObject line; //球线对象 9 public GameObject assistBall; //辅助球对象 10 public GameObject cueObject; //球杆的父节点,主要是起到简化球杆计算的方法 11 public GameObject cueBall; //母球对象 12 public Vector3 cuePosition; //用于储存球杆父节点位置的变量 13 ArrayList ballNeedRemove; //辅助删除列表 14 private float t = 0; //插值变量 15 private Result result; //结果类的引用 16 void Start () { 17 result = GetComponent("Result") as Result; 18 cuePosition = cueBall.transform.position; 19 isEightMode = PlayerPrefs.GetInt("billiard") < 9; //模式的标志位 20 isJudgeOver = PlayerPrefs.GetInt("billiard") == 9;//第一次击球是否结束的标志位 21 resetPositionFlag = false; //白球重置的标志位 22 ballNeedRemove = new ArrayList(); //创建删除列表 23 } 24 private void afterBallStopCallback() //摄像机跟随方法 25 { 26 if (CamControl.curCam == 1){ //如果为第一人称视角 27 if (resetPositionFlag){ //如该标志位为true 28 setData(); //设置数据 29 resetData(); //重置数据 30 }else{ 31 t = Mathf.Min(t + Time.deltaTime / 2.0f, 1); //进行插值计算 32 cueObject.transform.position = //计算cueObject的位置 33 Vector3.Lerp(cuePosition, cueBall.transform.position, t); 34 if (t == 1) { //等于1之后开始重新设置一些信息 35 setData(); 36 t = 0; 37 }}} else{ 38 setData(); 39 }} 40 void resetData() //重新设置各种信息 41 { 42 GameLayer.totalRotation = 0; //白球入洞后恢复位置 43 cueObject.transform.rotation = new Quaternion(1,0,0,13); //恢复球杆起始状态 44 (GetComponent("CamControl") as CamControl).setFreeCame(); //重置摄像机的信息 45 } 46 …//此处省略了Update方法,将在下面进行介绍 47 …//此处省略了Setdata方法,将在下面进行介绍 48 …//此处省略了removeBalls方法,将在下面进行介绍 49 }
第4行~第15行声明了该组件控制的游戏对象,包括球杆、辅助线、辅助球和母球等。同时声明了需要控制逻辑的脚本引用,以及游戏过程中相关的部分标志位的声明。
第16行~第23行为Start方法,在该方法中,获取组件并初始化球杆,初始化了部分游戏相关变量的标志位。包括第一次进球是否结束标志位,这里也分八球模式与九球模式,九球模式不需要进行第一次击球的判断,只要最后黑色八号球进洞即可。同时要为进洞的桌球创建一个删除列表。
第24行~第39行为摄像机跟随方法。如果是第一人称则慢慢跟随,如果是其他两人称,不慢慢跟随,直接重新设置球杆的位置,而不是摄像机的位置。
第40行~第45行主要为重新设置各种信息的方法,当游戏视角为第一人称时,白球进洞之后,要让球杆白球等回到起始点,同时调用CamControl类的setFreeCame方法重新设置摄像机的信息。
(16)下面介绍上述主控逻辑脚本代码中省略的Update方法和setData方法,前者代码主要负责实时判断桌球是否需要删除,是否全部停止运动。后者主要负责为八球模式判断应该击打哪种花色的桌球。详细代码如下。
代码位置:见随书光盘中源代码/第02章/Table3D/Assets/Scripts/GameScript目录下的Logic.cs。
1 void Update() 2 { 3 for (int i = 0; i < GameLayer.BallGroup_TOTAL.Count; i++) 4 { //循环遍历需要列表,找到需要删除球 5 GameObject tran = GameLayer.BallGroup_TOTAL[i] as GameObject; 6 BallScript ballScript = tran.GetComponent("BallScript") as BallScript; //获取脚本组件 7 if (ballScript.isAlowRemove) { //判断是否允许删除 8 ballNeedRemove.Add(tran); 9 } } 10 removeBalls(); //调用removeBalls方法,删除允许删除的桌球 11 if (!GameLayer.TOTAL_FLAG && !GameLayer.isStartAction){ 12 for (int i = 1; i < GameLayer.BallGroup_TOTAL.Count; i++) 13 { //循环判断所有桌球是否停止运动 14 GameObject obj = (GameLayer.BallGroup_TOTAL[i] as GameObject); 15 if (obj.rigidbody.velocity.sqrMagnitude > 0.01f) { //判断球是否停止 16 return; 17 }} 18 if (resetPositionFlag) { //如果白球掉落出球台 19 afterBallStopCallback(); //调用寻回方法 20 } 21 if (cueBall.rigidbody.velocity.sqrMagnitude < 0.01f) {//如果所有球都停止 22 afterBallStopCallback(); 23 }}} 24 private void setData() 25 { 26 if (resetPositionFlag){ 27 resetPositionFlag = false; //将标志位置反 28 cueBall.transform.position = ConstOfGame.CUEBALL_POSITION; //重新设置白球的位置 29 cueBall.transform.rigidbody.velocity = Vector3.zero; //重新设置白球的速度 30 cueBall.transform.renderer.enabled = true; //重新设置0号球的位置 31 } 32 if (!isJudgeOver) { //如果还是第一次击球 33 int one_count = GameLayer.BallGroup_ONE_EIGHT.Count;//八球模式桌球列表一 34 int two_count = GameLayer.BallGroup_TWO_EIGHT.Count;//八球模式桌球列表二 35 if (one_count == two_count){ //如果两个列表大小想等 36 ConstOfGame.kitBallNum = 0; //可以任意击球 37 PowerBar.tipIndex = 0; 38 }else if (one_count < two_count){ //列表一比列表二小 39 ConstOfGame.kitBallNum = 1; //表示可以击打花色球 40 isJudgeOver = true; //重置标志位,表示第一次击球结束 41 PowerBar.tipIndex = 1; 42 }else{ 43 ConstOfGame.kitBallNum = 2; //表示可以击打全色球 44 PowerBar.tipIndex = 2; 45 isJudgeOver = true; //重置标志位,表示第一次击球结束 46 }} 47 cueObject.transform.position = cueBall.transform.position; //设置球杆父节点的位置 48 cue.renderer.enabled = true; //设置球杆可见 49 line.renderer.enabled = true; //设置球线可见 50 GameLayer.TOTAL_FLAG = true; //运行触控以及按钮起作用 51 }
第3行~第10行主要负责删除进洞的桌球,首先遍历桌球列表,通过调用BallScript脚本,判断每一个桌球是否需要删除。如果删除标志位为true,则调用相应方法,将该桌球添加到删除列表。
第11行~第22行主要负责循环判断所有桌球是否停止运动,由于在击球过程中,会出现连环碰撞效果,只有当所有球全部停止运动,摄像机跟随白球到白球停止的位置,然后进入下一次击球。
第26行~第30行表示重置信息,将标志位置反的同时,重置白球的位置和速度。
第32行~第46行表示如果还是第一次击球,则根据两个数组的大小判断应该打几号球,相等时可以任意击球。
第47行~第50行表示设置球杆的相应操作,每次击球后,球杆会消失不见,摄像机跟随运动结束后,球杆和辅助线会显示。
(17)下面介绍上述主控逻辑脚本代码中省略的removeBalls方法,该部分代码主要负责删除桌球方法,同时根据进洞的桌球判断游戏的胜利和失败。其详细代码如下。
1 void removeBalls() 2 { 3 if (isEightMode) { //若游戏模式为八球模式 4 if (GameLayer.BallGroup_ONE_EIGHT.Count == 0 || //若八球模式列表一为0 5 GameLayer.BallGroup_TWO_EIGHT.Count == 0){ //若八球模式列表二为0 6 PowerBar.tipIndex = 4; //所有为4 7 } 8 for (int i = ballNeedRemove.Count -1; i >= 0; i--){ 9 GameObject tran = ballNeedRemove[i] as GameObject; 10 BallScript ballScript = tran.GetComponent("BallScript") as BallScript; //获取脚本组件 11 if (ballScript.ballId == 0) { //如果是0号球 12 resetPositionFlag = true; //重置白球位置的标志位变为ture 13 ballScript.isAlowRemove = false; //设置允许删除的标志位为false 14 tran.transform.renderer.enabled = false; //重新设置0号球的位置,出屏幕即可 15 tran.rigidbody.velocity = Vector3.zero; //设置白球的速度 16 tran.rigidbody.angularVelocity = Vector3.zero; 17 }else if (ballScript.ballId< 8){ 18 GameLayer.BallGroup_ONE_EIGHT.Remove(tran); //删除该组件 19 GameLayer.BallGroup_TOTAL.Remove(tran); 20 DestroyImmediate(tran); 21 GameLayer.ballInNum++; //进球数加一 22 if (ConstOfGame.kitBallNum == 2){ 23 result.goLoseScene(); //去往失败界面 24 }}else if (ballScript.ballId == 8){ 25 GameLayer.BallGroup_TOTAL.Remove(tran); //删除该组件 26 DestroyImmediate(tran); 27 if (GameLayer.BallGroup_ONE_EIGHT.Count == 0 || 28 GameLayer.BallGroup_TWO_EIGHT.Count == 0){ 29 result.goVectorScene(); //去往胜利界面 30 }else{ 31 result.goLoseScene(); //去往失败界面 32 }}else{ 33 GameLayer.BallGroup_TWO_EIGHT.Remove(tran); //删除该组件 34 GameLayer.BallGroup_TOTAL.Remove(tran); 35 DestroyImmediate(tran); 36 GameLayer.ballInNum++; //进球数加一 37 if (ConstOfGame.kitBallNum == 1){ 38 result.goLoseScene(); //去往失败界面 39 }}}}else{ 40 if (GameLayer.BallGroup_ONE_NINE.Count == 0) { //如果除黑色八号球全部进洞 41 PowerBar.tipIn dex = 4; //则提示可以击打黑色八号球 42 } 43 for (int i = ballNeedRemove.Count -1; i >= 0; i--){ 44 GameObject tran = ballNeedRemove[i] as GameObject; //获取物体 45 BallScript ballScript=tran.GetComponent("BallScript")as BallScript; //获取脚本组件 46 if (ballScript.ballId == 0) { //如果是0号球 47 resetPositionFlag = true; //重置白球位置的标志位变为 ture 48 ballScript.isAlowRemove = false; //设置允许删除的标志位为false 49 tran.transform.renderer.enabled = false; //重新设置0号球的位置,出屏幕即可 50 tran.rigidbody.velocity = Vector3.zero; //设置白球的线速度与角速度 51 tran.rigidbody.angularVelocity = Vector3.zero; 52 }else if (ballScript.ballId == 8){ 53 GameLayer.BallGroup_ONE_NINE.Remove(tran); //删除该组件 54 GameLayer.BallGroup_TOTAL.Remove(tran); 55 DestroyImmediate(tran); 56 if (GameLayer.BallGroup_ONE_NINE.Count == 0){ 57 result.goVectorScene(); //去往胜利界面 58 }else{ 59 result.goLoseScene(); //去往失败界面 60 }}else{ 61 PowerBar.tipIndex =3; 62 GameLayer.BallGroup_ONE_NINE.Remove(tran); //删除该组件物体 63 GameLayer.BallGroup_TOTAL.Remove(tran); 64 DestroyImmediate(tran); 65 GameLayer.ballInNum++; //进球数加一 66 }}} 67 ballNeedRemove.Clear(); //清空列表 68 }
第3行~第7行表示当游戏为八球模式的时候,进行提示的判断,如果1~7号球或者9~15号球全部进洞,则提示可以击打黑色八号球。
第8行~第23行为判断桌球是否可以从桌球列表删除的方法。如果桌球ID为0,重新设置白球位置速度,桌球不许删除;当桌球ID小于8的时候,表示全色球进洞,而此时可为进球数加一,但是若此时击球种类全局变量为2,表示进球出错,游戏失败。
第24行~第39行为判断桌球是否可以从桌球列表删除的方法。当ID等于8的时候,即为黑色八号球入洞。而此时,若八球模式的花色球和全色球列表都清零,游戏即为胜利,否则游戏失败。当ID大于8的时候,表示花色球进洞,而此时可为进球数加一;但是若此时击球种类全局变量为1,表示进球出错,游戏失败。
第43行~第67行表示当游戏为九球模式的时候,同样根据ID号进行判断。如果桌球ID为0,重新设置白球位置速度,桌球不许删除;当ID等于8的时候,即为黑色八号球入洞。而此时,若八球模式的花色球和全色球列表都清零,游戏即为胜利,否则游戏失败。