【3D游戏编程与设计】二 离散引擎基础 & 3D井字棋
简答题
解释 游戏对象(GameObjects) 和 资源(Assets)的区别与联系。
- 游戏对象(GameObjects):GameObject是Unity中的基类对象,其是所有其他游戏对象的基类,其包括很多抽象方法。包括玩家,敌人,游戏场景,摄像机等类,可以实现这些抽象方法,成为可以被实例化的对象。我们可以在游戏中使用这些可以被实例化的对象。这些对象由资源组成,是游戏运行过程中动态的,可以交互的实体。
- 资源(Assets):是游戏包括的素材资源, 他们可以被多个对象使用,有些资源可以做为预设并实例化为多个对象。资源通常包括预设,脚本,材料,场景,声音等等。
下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)
- 资源的目录组织结构:主要包括动画,文本,场景,素材,材料,模型,预设以及说明文档等。
- 游戏对象树的层次结构: 主要包括摄像机,光源,游戏开始位置,场景布局以及文本内容等。
编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件
public class TryDebug : MonoBehaviour {
void Start (){
Debug.Log ("onStart");
}
// Update is called once per frame
void Update (){
Debug.Log ("onUpdate");
}
void Awake(){
Debug.Log ("onAwake");
}
void FixedUpdate(){
Debug.Log ("onFixedUpdate");
}
void LateUpdate(){
Debug.Log ("onLateUpdate");
}
void OnGUI(){
Debug.Log ("onGUI");
}
void OnDisable(){
Debug.Log ("onDisable");
}
void OnEnable(){
Debug.Log ("onEnable");
}
}
查找脚本手册,了解 GameObject,Transform,Component 对象
分别翻译官方对三个对象的描述(Description)
GameObjects是Unity中代表角色,道具和风场景的基本对象。它们本身并不是很完整,但它们充当组件的容器实现了具体的功能。
Transform变换组件决定了每个对象在场景中的位置,比例和旋转,每个对象都有一个变换组件。
Component是用来绑定到游戏对象(Game Object)上的一组相关属性。本质上每个组件是一个类的实例。
描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件
table的对象是GameObject,第一个复选框是activeSelf 属性,第二个文本框对应对象的名称,第三个复选框为static属性。第二行有Tag属性和Layer属性,第三行为prefabs属性,也就是预设属性。Transform属性包括position(位置),Rotation(旋转角度),Scale(大小比例)等。
其包括的Component部件有Transform,Mesh Filter,Box Collider,Mesh Renderer。
用 UML 图描述 三者的关系
三者的关系如上图。
资源预设(Prefabs)与 对象克隆 (clone)
预设(Prefabs)有什么好处?
1.使对象能够很方便地重复利用,不需要多次手动创建。
2. 相同的游戏对象可以用同一个预设建模并创建。
3. 对预设进行更新后,由该预设创建的游戏对象都可以很方便地更新。
4. 通过代码直接加载预设也非常方便。
5. 预设的使用使得整个游戏项目更有维护性和可扩展性。
6. 预设资源完整储存了对象的组件和属性。可以使用预设作为一个模板,方便我们创建相同的游戏对象并赋予他们相同的行为,比如用棋盘格子创建一个很大的棋盘。
预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
克隆游戏对象需要场景中有被克隆对象,而创建预制只需assets中有预设即可,不需要场景中存在对应的游戏对象。
对象克隆出来的对象并不会随着母体的变化而变化,但预设创建出来的对象会随着预设的更新而发生改变。
制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
可以按照文档的指引创建名为table的桌子预设。
GameObject mytable = Instantiate(Resources.Load("table"), new Vector3(0, 0, 0), Quaternion.identity) as GameObject;
编程实践,小游戏
编程实践了3D井字棋小游戏。并且完善其,使得其可以多次运行,并且可以有效避免多种用户非法操作的情况,增加了项目的健壮性。
github上项目网址https://github.com/alphabstc/TicTacToe
下载上面的项目可以直接运行。
(gitee上传不了这么大的项目)
创建材料和预设
首先,在Assets文件夹下创建Resources,Scenes和Scripts三个文件夹。
之后在Resources文件夹中创建上图所示的4种颜色的材料(名称为black,blue,red,white)。
之后,创建两个3D立方体,将black和white材料分别拖上去,得到黑色和白色的立方体。分别命名为Black Cube和White Cube。之后将他们从对象层次视图中拖到Resources文件夹中,成为预设。他们将作为棋盘格子的预设。
同样,创建两个3D球体,将red和blue材料分别拖上去,得到红色和蓝色的球体。分别命名为Red Chess和Blue Chess。之后将他们从对象层次视图中拖到Resources文件夹中,成为预设。他们将作为红蓝两方棋子的预设。
创建脚本
创建名为Chessboard的空对象,并且创建名为Chessboard的脚本,将Chessboard脚本拖到Chessboard空对象上。Chessboard脚本的内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Chessboard : MonoBehaviour{//棋盘类
private GameObject[,] Chesses = new GameObject[3, 3];//存放棋盘格子对象的数组
private int[,] value = new int[3,3];//存放棋盘每个格子状态的数组
private int counter = 0;//计数器 当前棋盘上有多少个棋子
private GameObject[] chess = new GameObject[9];//存放棋子对象的数组
private bool gameover = true;//是否游戏结束
// Start is called before the first frame update
void Start(){
Debug.Log("Start");
//创建所有棋盘格子对象
for(int i = 0;i < 3;i++){
for(int j = 0;j <3;j++){
if (i%2 != j%2){//根据坐标创造黑白相间的棋盘格子
Chesses[i, j] = Instantiate(Resources.Load("White Cube"), new Vector3(i, 0, j), Quaternion.identity) as GameObject;
}
else
{
Chesses[i, j] = Instantiate(Resources.Load("Black Cube"), new Vector3(i, 0, j), Quaternion.identity) as GameObject;
}
Chesses[i, j].transform.parent = this.transform;
}
}
Restart();
}
void Restart(){//重新开始游戏
for(int i = 0; i < counter; i++)
Destroy(chess[i]);//销毁棋子
counter = 0;//计数器清零
for(int i = 0; i < 3;i++){
for(int j = 0; j <3;j++){
//初始化棋盘状态
value[i, j] = 100 + i * 10 + j;//为超过100且互不相等的数 则都不会相等 不会触发胜利条件
}
}
gameover = false;
}
// Update is called once per frame
void Update(){
}
bool judgeWin(){
//同一行 同一列 或者对角线上的状态相等 说明有玩家胜利
for(int i = 0; i < 3; ++i)
if (value[i, 1] == value[i, 0] && value[i, 2] == value[i, 0])
return true;
for(int i = 0; i < 3; ++i)
if (value[1, i] == value[0, i] && value[2, i] == value[0, i])
return true;
if (value[1, 1] == value[0, 0] && value[2, 2] == value[0, 0])
return true;
if (value[1, 1] == value[0, 2] && value[2, 0] == value[0, 2])
return true;
return false;
}
void OnGUI(){
//生成一个菜单栏
GUI.Box (new Rect (10, 10, 100, 50), "菜单");
if (GUI.Button (new Rect (20, 35, 80, 20), "新游戏")) {
Restart();//按下新游戏键则重新开始游戏
}
//判断是否有玩家胜利
bool result = judgeWin();
if (result){//有玩家胜利
//胜利的是最后一个落子的玩家
GUI.Box(new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 50), counter % 2 == 0 ? "蓝方胜" : "红方胜");
gameover = true;
}
else{
if (counter == 9){//没有胜利 但已经下完了9个棋子 则平局
GUI.Box (new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 50), "平 局");
gameover = true;
}
}
}
public void NewChess(GameObject grid){
if (gameover)//游戏已经结束
return;
//已经放过了棋子
if (value[(int)(grid.transform.position.x + 1e-5), (int)(grid.transform.position.z + 1e-5)] < 2)
return;
if (counter % 2 == 0){
//是红方落子
Debug.Log("Click Red");
chess[counter++] = Instantiate(Resources.Load("Red Chess"), grid.transform.position + new Vector3(0, 1, 0), Quaternion.identity) as GameObject;
value[(int)(grid.transform.position.x + 1e-5), (int)(grid.transform.position.z + 1e-5)] = 0;
}
else{
//是蓝方落子
Debug.Log("Click Blue");
chess[counter++] = Instantiate(Resources.Load("Blue Chess"), grid.transform.position + new Vector3(0, 1, 0), Quaternion.identity) as GameObject;
value[(int)(grid.transform.position.x + 1e-5), (int)(grid.transform.position.z + 1e-5)] = 1;
}
}
}
之后,创建名为ClickGrid的脚本,将其拖到Black Cube和White Cube两种预设上。编辑ClickGrid脚本的内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickGrid : MonoBehaviour{
// Start is called before the first frame update
void Start(){
}
// Update is called once per frame
void Update(){
}
void OnMouseUpAsButton(){
Debug.Log("Click");
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//通过射线来检测是否碰撞
RaycastHit hit;
if(Physics.Raycast(ray, out hit)){//和棋盘格子碰撞
Debug.Log("Click Success");
GameObject grid = hit.collider.gameObject;//获得碰撞的对象
GameObject.Find("Chessboard").SendMessage("NewChess", grid);//调用棋盘对象的NewChess函数
}
}
}
调整摄像头
调整摄像头的位置和姿态,使得其可以以比较好的视角看见创建后的棋盘。
这样就完成了项目的配置。
效果结果
AI思路
考虑到可以使用动态规划实现AI井字棋。f[mask]表示在当前棋盘二进制为mask的状态下,是先手必胜还是后手必胜还是平局。可以使用动态规划求出结果。
由于前几天考试周,没有太多时间实现,留坑以后再实现。