Skip to content

学习笔记1-GUI井字棋

1. 简答题

  • 解释 游戏对象(GameObjects) 和 资源(Assets)的区别与联系。

    资源(Assets)是包含在游戏文件中,为游戏对象所使用的,一个资源可以被一个或多个对象使用。而对象出现在游戏场景(Scene)中,是Components、资源整合在一起的结果。

  • 下载几个游戏案例,分别总结资源、对象组织的结构

    分析下载的游戏案例

    1.资源文件夹通常有对象、材质、场景、声音、预设、贴图、脚本、动作,在这些文件夹下可以继续划分。

    2.对象中一般有玩家、敌人、环境、摄像机和音乐等虚拟父类,这些父类本身没有实体,但他们的子类包含了游戏中会出现的对象。

  • 编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件

    • 基本行为包括 Awake() Start() Update() FixedUpdate() LateUpdate()
    • 常用事件包括 OnGUI() OnDisable() OnEnable()
public class testBasicBehaviour : MonoBehaviour {
    void Awake() {
        Debug.Log ("onAwake");
    }
    void Start () {
        Debug.Log ("onStart");
    }
    void Update () {
        Debug.Log ("onUpdate");
    }
    void FixedUpdate() {
        Debug.Log ("onFixedUpdate");
    }
    void LateUpdate() {
        Debug.Log ("onLateUpdate");
    }
    void OnGUI() {
        Debug.Log ("onGUI");
    }
    void Reset() {
        Debug.Log ("onReset");
    }
    void OnDisable() {
        Debug.Log ("onDisable");
    }
    void OnDestroy() {
        Debug.Log ("onDestroy");
    }
}
  • 查找脚本手册,了解 GameObject,Transform,Component 对象

    • 分别翻译官方对三个对象的描述(Description)

      • 游戏对象在Unity中是代表着游戏物件、道具以及游戏场景的基本对象。它们本身没有完成太多内容,但它们充当着组件的容器,让开发者实现真正的功能。
      • Transform属性定义了组件的位置,旋转角度,各个方向的大小比例(缩放)。每一个游戏对象(GameObject)都有着一个Transform。
      • 组件是游戏中物体和行为的螺母和螺栓, 它们是每个游戏对象的功能块。一个游戏对象是许多不同的组件的容器。默认情况下,所有的游戏对象自动拥有Transform组件。这是因为Transform组件决定了游戏对象的位置,以及它如何旋转和缩放。没有Transform组件的游戏对象,不会在World中存在。
    • 描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的 部件 ch02-homework

      该table对象的属性(component)有如下: > 1. Transform描述了table对象的位置,旋转角度,缩放, 图中的属性为,位置(0,0,0), 旋转(0,0,0),比例为(100%, 100%, 100%);

      1. Mesh Filter网格过滤器描述了几何体的形状,图中table为Cube(立方体)

      2. Box Colider则描述了物体的碰撞范围,图中的碰撞范围为以(0,0,0),(1,0,0),(0,1,0),(1,1,0),(0,0,1),(1,0,1),(0,1,1),(1,1,1)为顶点的一个正方体

      3. Mesh Renderer网格渲染器则从网格过滤器中获得几何体的形状然后根据要求进行渲染。图中的属性为 Cast Shadows(投射阴影方式)开启,接受阴影 Motion Vectors(运动向量生成方式)为PerObject Motion(会渲染出一个“每个对象”运动向量通道),材质为默认材质,Light Probes(光探头插值方式)以及Reflection Probes(反射探头都为混合),Anchor Override为当使用光探头或反射探头系统时,用来决定插值位置的Transform,图中未定义该Transform。

    • 用 UML 图描述 三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图) new

  • 整理相关学习资料,编写简单代码验证以下技术的实现:(查找对象、添加子对象、遍历对象树、清除所有子对象)

//查找对象
public static GameObject Find(string name)//通过名字查找
public static GameObject FindWithTag(string tag)//通过标签查找单个对象
public static GameObject[] FindGameObjectsWithTag(string tag)//通过标签查找多个对象
//添加子对象
public static GameObect CreatePrimitive(PrimitiveTypetype)
//遍历对象树
foreach (Transform child in transform) {
     Debug.Log(child.gameObject.name);
}
//清除所有子对象
foreach (Transform child in transform) {
      Destroy(child.gameObject);
}
  • 预设(Prefabs)有什么好处?与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?

    Unity通过预设(Prefabs)完整地储存了对象的组件、属性等内容,方便开发者创建具有相同属性的对象,方便了对象的复用。对象克隆与预设不同的地方就在于,预设与实例化的对象是一致的,预设发生变化,所有通过其实例化的对象都会产生变化,而对象克隆本体和克隆出的对象不同,其不受本体改变影响。

  • 尝试解释组合模式(Composite Pattern / 一种设计模式)。使用 BroadcastMessage() 方法

    • 向子对象发送消息

    组合模式允许用户将对象组合成树形结构来表现”部分-整体“的层次结构,使得客户以一致的方式处理单个对象以及对象的组合。组合模式实现的最关键的地方是——简单对象和复合对象必须实现相同的接口。这就是组合模式能够将组合对象和简单对象进行一致处理的原因。

//父对象
public class child: MonoBehaviour {
    void Start () {
        this.BroadcastMessage("getMessage", "test");
    }

    void getMessage(string msg) {
        Debug.Log("Get msg: " + msg);
    }
}
//子对象
public class child: MonoBehaviour {
    void getMessage(string msg) {
        Debug.Log("Get msg: " + msg);
    }
}

2.编程实践,小游戏

  • 实践部分选择的游戏内容为: 井字棋

    首先,根据老师的只能使用IMGUI构建UI的要求,学习了OnGUI的用法,得知OnGUI就是一个Unity提供的用于绘制每一帧界面的函数,和Update一样在每一帧都会被调用。理解原理之后就十分简单了,我们只需要维护好棋盘的状态,记录好棋盘的数据,然后通过GUI函数把当前的棋盘渲染出来就可以了,编程难度并不大,所以首先在类中定义好需要用到的数据,并完成初始化的函数

private int[,] gameBoxStatus = new int[3, 3]; //用于保存棋盘数据
private int gameStatus = 0; //记录游戏状态
private int term = 1; //记录当前轮到哪个玩家

void initialGameInfo () {
    gameStatus = 0;
    term = 1;
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            gameBoxStatus[i,j] = 0;
}

写好这个之后就是OnGUI部分的开发了,其实关键的就是根据二维数组中的数据用一个两重循环渲染出棋盘,OnGUI代码如下

void OnGUI () {
    GUIStyle headerStyle = new GUIStyle ();
    headerStyle.normal.textColor = new Color (255, 255, 255);
    headerStyle.fontSize = 40;
    GUIStyle gameInfoStyle = new GUIStyle ();
    gameInfoStyle.normal.textColor = new Color (255, 255, 255);
    gameInfoStyle.fontSize = 25;
    GUI.Label(new Rect(300,60,200,100), "Simple Chess Game", headerStyle);

    /**
    * gameStatus == 0, 代表游戏状态为选择谁先下的状态,未开始游戏;
    * gameStatus == 1, 代表游戏已开始,渲染出棋盘
    * gameStatus == 3, 代表游戏已经结束,渲染出游戏结果
    */

    if (gameStatus == 0) {
        GUI.Label (new Rect (xpos, ypos, 100, 50), "Who first?", gameInfoStyle);
        if (GUI.Button (new Rect (xpos, ypos + 60, 100, 50), "O")) {
            term = 1;
            gameStatus = 1;
        }
        if (GUI.Button (new Rect (xpos, ypos + 120, 100, 50), "X")) {
            term = 2;
            gameStatus = 1;
        }
    } else if (gameStatus == 1) {
        if (GUI.Button (new Rect (xpos+25, ypos + 180, 100, 50), "Reset"))
            initialGameInfo ();
        int result = checkWin ();//检查是否产生游戏结果
        if (result == 1) {
            Winner = 1;
            gameStatus = 2;
        } else if (result == 2) {
            Winner = 2;
            gameStatus = 2;
        } else if (result == 3) {
            Winner = 0;
            gameStatus = 2;
        }
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++) {
                if (gameBoxStatus [i, j] == 1)
                    GUI.Button (new Rect (xpos + i * 50, ypos + j * 50, 50, 50), "O");
                if (gameBoxStatus [i, j] == 2)
                    GUI.Button (new Rect (xpos + i * 50, ypos + j * 50, 50, 50), "X");
                if (GUI.Button (new Rect (xpos + i * 50, ypos + j * 50, 50, 50), "")) {  
                    if (result == 0) {  
                        gameBoxStatus [i, j] = term;  
                        term = (term == 2) ? 1 : 2;
                    }  
                }  
            }
    } else {
        if (Winner == 0) {
            GUI.Label (new Rect (xpos+10, ypos, 100, 50), "No one wins!", gameInfoStyle);
        } else if (Winner == 1) {
            GUI.Label (new Rect (xpos+10, ypos, 100, 50), "O wins!", gameInfoStyle);
        } else {
            GUI.Label (new Rect (xpos+10, ypos, 100, 50), "X wins!", gameInfoStyle);
        }
        if (GUI.Button (new Rect (xpos, ypos+70, 100, 50), "Reset"))
            initialGameInfo ();
    }
}

在完成了OnGUI以及初始化函数后,就剩下CheckWin函数了,其实也比较简单具体代码如下,就是循环各个结果看有没满足胜利条件,以及判断当前是否平局,返回游戏结果(0:游戏未结束,1:胜者为1(O), 2: 胜者为2(X))

int checkWin () {
    for (int i = 0; i < 3; i++) {
       if (gameBoxStatus [0, i] != 0 && gameBoxStatus [0, i] == gameBoxStatus [1, i] && gameBoxStatus [1, i] == gameBoxStatus [2, i])
           return gameBoxStatus [0, i];
       if (gameBoxStatus [i, 0] != 0 && gameBoxStatus [i, 0] == gameBoxStatus [i, 1] && gameBoxStatus [i, 1] == gameBoxStatus [i, 2])
           return gameBoxStatus [i, 0];
    }
    if ((gameBoxStatus [1, 1] != 0 && gameBoxStatus [0, 0] == gameBoxStatus [1, 1] && gameBoxStatus [1, 1] == gameBoxStatus [2, 2]) ||
      (gameBoxStatus [0, 2] == gameBoxStatus [1, 1] && gameBoxStatus [1, 1] == gameBoxStatus [2, 0]))
        return gameBoxStatus [1, 1];
    bool flag = true;
    for (int i = 0; i < 3; i++)
       for (int j = 0; j < 3; j++) {
          if (gameBoxStatus[i,j] == 0) {
              flag = false;
          }
       }
    return (flag) ? 3 : 0;
}

V2版本中新加入了计分的功能,实现部分也十分简易,在本类中添加Oscore, Xscore变量,然后通过对这两个变量的维护即可完成该功能,值得注意的是,OnGUI是每一帧刷新的,所以决出胜者之后,我们应该保证增加score的代码只被执行一次。代码就不再在这里赘述了,有兴趣的可以通过我的github查看完整的代码,传送门就在页尾🙃

到这里整个游戏就完成了,游戏的整个代码我就不在这里贴出来了,访问我的Github就可以看到代码以及演示视频了,下面是链接:github@zhongwq/Unity3D-Learning/week1