Unity游戏案例开发大全
上QQ阅读APP看书,第一时间看更新

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的时候,即为黑色八号球入洞。而此时,若八球模式的花色球和全色球列表都清零,游戏即为胜利,否则游戏失败。