原创文章,请勿转载。感谢!!
适合人群
必须会Unity3D引擎基础,对C#脚本编程有一定经验
使用插件
本案列使用前还必须导入以下这些插件
DoTween
优秀的动画插件
FairyGUI
本案例用的是FairyGUI系统,基础知识可以到FairyGUI官网学习,当然你可以将UI改造为UGUI系统。
游戏介绍
太空战机是一款通过识别对应的AR游戏卡片结合实景的敏捷躲避AR游戏,游戏采用UNITY3D引擎和EasyAR联合开发。游戏主要操作方法是利用手机重力感应来控制飞机左右移动躲避障碍物,并且有各种各样的道具辅助玩家获得高分数排名。
游戏前期策划
游戏初期策划是占一个重要成分,前期策划做得好,后期游戏的制作流程会更加明了,我们可以先坐下来,用笔或纸记录自己的一点一点的创意,最后整理集合成一个游戏策划方案。
对于太空战机这个游戏,由于玩法比较简单,所以策划方案也不会太复杂,游戏策划大概列出下面这几点:
- 利用EasyAR识别图片后,出现开始界面,并且出现飞机,此时飞机不可以被玩家控制
- 点击开始按钮后进入倒数界面,倒数5秒,出现游戏界面,此时飞机可以被玩家控制
- 用重力感应控制飞机左右移动
- 圆环和道具从远处不断向飞机直线移动,碰到圆环损失1血,从圆环中心通过,奖励1分
- 道具分别是能量包和陨石,能量包用于恢复生命,陨石碰撞到后损失2血
- 当血量为0时,游戏结束,出现排行版
当然这个初期策划方案的功能并不是十分完善,例如后期会增加飞机选择界面,增加购买飞机商城,增加更多的道具,这些需由你自己去完4善策划内容,这样你会从中学习到更多的策划经验。
游戏素材准备
通常我们做游戏前,都需要准备制作该游戏的一些美术资源,音效资源,如果平时有收集整理自己的素材库,那素材的问题很快可以解决,如果平时没有这个习惯,可以到UNITY官方资源商店购买下载合适的素材,本案例中的资源包都会通过百度盘共享,可以直接拿来使用。
申请AppKey
我们首先要到EasyAR官网注册账号,然后进入开发中心,创建一个新应用,应用名为ARSpaceShip,注意Bundle ID(iOS) Package Name(Android)这一列,其格式通常为com.公司名.项目名,所以我们这里按照自己的实际需求来填写,本案例填写为com.game4u.arspaceship,最后点击显示按钮,出现AppKey。
如何制作识别度高的识别图
识别图如果识别度比较高,那么识别后,模型相对会稳定,并且不会出现抖动现象,那么我们应该怎样制作识别度高得识别图呢?以下我总结出来几点经验:
- 避免重复元素大量出现:大量圆圈、方形、菱形或者其他相同的形状,容易出现识别错误
- 避免图案元素分布不均匀:图案有区域出现大片空白、单一颜色或者分布不均匀,会导致识别错误
- 避免图案元素过于简单:识别跟踪更喜欢“杂乱无章”的图案,太简单的图案,有可能会导致扫描错误
- 避免图案全是文字:图案全部都是文字,扫描识别图有可能会出现识别错误
- 避免图案明暗对比度不明显:如果图案全是亮色或者暗色,有可能会导致识别错误
- 避免图案模糊:图案需要清晰的细节才能扫描,如果细节很模糊,识别图有可能扫描出错
- 避免图案高度反光: :如果图案反光严重,很容易导致识别图扫描出错
- 避免图案上印有二维码:请将二维码区域设置为空白。当识别图提供给用户扫描的时候,可以在空白区域补充二维码,不影响识别结果
- 避免使用png格式图
同样,本案例的识别图可以在资源包里找到。
制作识别图数据
我们先来制作一下数据文件。这个数据文件是一个json数据,我们输入以下内容,并保存文件为targets.json。
{"images" :
[
{
"image" : "arspaceship.jpg",
"name" : "arspaceship"
}
]
}```
****
#游戏框架初步搭建
素材准备好了后,我们开始制作太空战机AR游戏,打开Unity引擎并创建一个新项目,首先将下载好的EasyAR的unity package导入到项目里,如图
![导入EasyAR unity package](http://upload-images.jianshu.io/upload_images/3353184-8b068541ebb29051.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
导入完成后,我们右键Asset文件夹,新建一个自己的游戏文件夹,命名为ARSpaceShip,在这个文件夹里再分别建立Scene(用于放置场景文件),Scripts(用于放置脚本),Modles(用于放置模型),Textures(用于放置贴图),Materials(用于放置材质),Prefabs(用于放置预置物)。
如果项目里没有StreamingAssets和Resources文件夹,我们也应该右键新建此文件夹,如图
![建立文件夹](http://upload-images.jianshu.io/upload_images/3353184-335eb2b33ecb7823.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
现在我们在Scene文件夹里新建一个场景,命名为GameScene,双击打开该场景,删除掉里面的MainCamera和Diretorly Light,找到EasyAR的Prefab目录,将EasyAR_Startup组件拖到场景里,然后填写在官网里注册的AppKey,如图
![填写AppKey](http://upload-images.jianshu.io/upload_images/3353184-cf736ee08088efdf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
此时我们还需修改一下EasyAR_Startup里面的RenderCamera相机组件的视野属性,将Clipping Planes的Near和Far分别修改为 0.01和2000,如下图:
![RenderCamera属性](http://upload-images.jianshu.io/upload_images/3353184-2f4eba3c7c6da899.png)
我们将识别图和target.json一起放到StreamingAssets文件夹里,EasyAR会通过脚本访问此文件夹获得资源和数据。接下来同样在EasyAR的Prefab目录下的Primitives里将ImageTarget组件拖到场景里,并且將ImageTargetBehaviour脚本移除。
![5.png](http://upload-images.jianshu.io/upload_images/3353184-f72b8da11a91a2c9.png)
然后在我们的项目文件夹的Scripts目录右键,新建一个EasyImageTargetBehaviour脚本,输入以下代码:
using UnityEngine;
namespace EasyAR
{
public class EasyImageTargetBehaviour : ImageTargetBehaviour
{
protected override void Awake()
{
base.Awake();
TargetFound += OnTargetFound;
TargetLost += OnTargetLost;
}
protected override void Start()
{
base.Start();
HideObjects(transform);
}
void HideObjects(Transform trans)
{
for (int i = 0; i < trans.childCount; ++i)
HideObjects(trans.GetChild(i));
if (transform != trans)
gameObject.SetActive(false);
}
void ShowObjects(Transform trans)
{
for (int i = 0; i < trans.childCount; ++i)
ShowObjects(trans.GetChild(i));
if (transform != trans)
gameObject.SetActive(true);
}
void OnTargetFound(ImageTargetBaseBehaviour behaviour)
{
ShowObjects(transform);
}
void OnTargetLost(ImageTargetBaseBehaviour behaviour)
{
HideObjects(transform);
}
}
}
把这段代码拖到ImageTarget中,这段代码的作用是EasyAR如果识别到图片后,控制ImageTarget里面的所有子物体的显示和隐藏。然后填写一下ImageTarget属性面版里面的一些属性,注意Storage需要选择为Asset模式,如图:
![输入识别图信息](http://upload-images.jianshu.io/upload_images/3353184-d059e9bd65273eb6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**Storages说明**
>**App**
app路径,对于不同设备会有不同路径。
Android: 程序持久化数据目录(注意,这个目录与Unity的Application.persistentDataPath可能不同)
iOS: 程序沙盒目录
Windows: 可执行文件(exe)目录
Mac: 可执行文件目录(如果app是一个bundle,这个目录在bundle内部)
>**Assets**
StreamingAssets路径
>**Absolute**
绝对路径(json/图片路径或视频文件路径)或url(仅视频文件)
>**Json**
表示json string的标志位,在Target.Load中使用
大家有没发现上面的ImageTarget没有材质,不太好看,那么我们可以在自己项目文件夹里的Matertials文件夹右键新建一个材质ImageTargetMat,然后拖到ImageTarget的MeshRenderer组件的Materials里面,改变材质为Legacy Shaders/Self-Illumin/Diffuse,把识别图复制一份到Textures文件夹里拖到材质里面,如图:
![改变ImageTarget材质](http://upload-images.jianshu.io/upload_images/3353184-389f13a88fbbbad5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
最后,我们在ImageTarget里建立一个空的GameObject,命名为GameWorld,此时游戏初步框架完成。
![建立GameWorld](http://upload-images.jianshu.io/upload_images/3353184-4ccb4e200f0173b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
****
#制作游戏地面
在GameWorld里新建一个GameObject命名为Map,然后在里面新建一个Quad物体命名为Ground,调整他的Rotation X轴为90,目的是让它平放。我们再在Materials文件夹里新建一个GroundMat材质,将这个材质拖动到Ground物体,最后将识别图作为贴图赋值给材质,并调整合适大小,如图:
![制作游戏地面](http://upload-images.jianshu.io/upload_images/3353184-88b533980c89fcf0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
****
#制作太空战机
在GameWorld里新建一个GameObject命名为SpaceShipCotroller,并设置tag为Player,然后在素材包里面找到太空战机的模型的文件夹,将它复制到Modles文件夹里,将模型文件拖动到SpaceShipCotroller里作为子物体并将模型的Scale调为0.008,0.008,0.008,如图:
![飞机模型](http://upload-images.jianshu.io/upload_images/3353184-f4a1dfde696b34c9.png)
此时如果没有材质,可以将材质重新拖到飞机模型上关联即可。我们再调整一下SpaceShipController的物体的Y轴位置为0.13,Z轴位置为-0.28,如图:
![调整飞机大小位置](http://upload-images.jianshu.io/upload_images/3353184-bc747c92e1faf963.png)
在Scripts文件夹里新建一个脚本,命名为SpaceShipController,拖动到SpaceShipController物体处,输入以下代码:
using System;
using UnityEngine;
public class SpaceShipController : MonoBehaviour
{
//移动速度
public float Speed = 20f;
//方向
private Vector3 _dir = Vector3.zero;
void Start()
{
}
void Update()
{
Move();
}
/// <summary>
/// 飞机移动逻辑
/// </summary>
void Move()
{
_dir.x = Input.acceleration.x;
if (_dir.sqrMagnitude > 1) _dir.Normalize();
transform.Translate(Time.deltaTime * _dir * Speed);
}
}
这段代码的作用是用来控制太空战机。我们策划的时候是要通过手机重力感应来控制战机左右移动,UnityAPI提供了移动设备重力感应方法`Input.acceleration`,只需要通过x值来判断左右移动。我们还需要在SpaceShipController的属性面板中输入Speed的值,并且调试一下是否是自己想要的战机移动速度,本案例的速度填写为500,然后在手机上发布看看实际效果。此时你扫描识别图后,左右摇动手机,战机也跟着左右移动了。
现在看起来战机移动没问题,但是给人感觉有点生硬,需要加一个左右移动的时候机身会跟着侧飞,那么我们选中场景中的SpaceShipController物体,右键新建一个GameObject命名为RotateObject,然后把我们的模型拖到RotateObject里作为子物体,如下图:
![RotateObject](http://upload-images.jianshu.io/upload_images/3353184-374aab3aee1d63b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
双击SpaceController脚本,给它增加两个属性:
//旋转速度
public float RotateSpeed = 20f;
//需要旋转的物体
public Transform RotateObject;
然后增加多一个侧飞的方法:
/// <summary>
/// 侧飞
/// </summary>
void Rotate()
{
//根据方向判断是左旋转还是右旋转
if (_dir.x < 0)
{
RotateObject.localRotation = Quaternion.Lerp(RotateObject.localRotation,Quaternion.Euler(new Vector3(0, 0, 30f)), Time.deltaTime * RotateSpeed);
}
else
{
RotateObject.localRotation = Quaternion.Lerp(RotateObject.localRotation,Quaternion.Euler(new Vector3(0, 0, -30f)),Time.deltaTime * RotateSpeed);
}
}
然后在Update方法处,添加Rotate方法:
void Update()
{
Move();
Rotate();
}
最终的SpaceController脚本应该如下:
using System;
using UnityEngine;
public class SpaceShipController : MonoBehaviour
{
//移动速度
public float Speed = 20f;
//方向
private Vector3 _dir = Vector3.zero;
//旋转速度
public float RotateSpeed = 20f;
//需要旋转的物体
public Transform RotateObject;
void Start()
{
}
void Update()
{
Move();
Rotate();
}
/// <summary>
/// 飞机移动逻辑
/// </summary>
void Move()
{
_dir.x = Input.acceleration.x;
if (_dir.sqrMagnitude > 1) _dir.Normalize();
transform.Translate(Time.deltaTime * _dir * Speed);
}
/// <summary>
/// 侧飞
/// </summary>
void Rotate()
{
if (_dir.x < 0)
{
RotateObject.localRotation = Quaternion.Lerp(RotateObject.localRotation, Quaternion.Euler(new Vector3(0, 0, 30f)),
Time.deltaTime * RotateSpeed);
}
else
{
RotateObject.localRotation = Quaternion.Lerp(RotateObject.localRotation, Quaternion.Euler(new Vector3(0, 0, -30f)),
Time.deltaTime * RotateSpeed);
}
}
}
写完代码后,再次点击场景中的SpaceShipController物体,会发现多了RotateSpeed和RotateObject属性,先设置RotateSpeed为2,再将场景中的RotateObject拖动到RotateObject属性l里,如图:
![设置属性](http://upload-images.jianshu.io/upload_images/3353184-6d8e6e4a247ff9a9.png)
现在飞机看起来没有倒影显得不是那么真实,我们可以再素材库将SpaceShipShadow.png复制到Textures文件夹下,在场景中的SpaceShipController物体右键新建一个Quad物体命名为Shadow,去掉组件MeshCollider,在Materials文件夹中新一个SpaceShipShadow材质,将SpaceShipShadow.png拖到材质里然后再设置为透明材质,设置完成后将材质关联给场景中Shadow物体,最后调整Shadow物体的Position Y轴为-0.2, Rotation X轴为90度,Scale为0.16,0.16,0.16,效果如下图:
![调整效果](http://upload-images.jianshu.io/upload_images/3353184-f3d41697a4b26de0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
飞机飞行的时候还需要有个喷射效果,在Modles文件夹的SpaceShip文件夹找到SpaceShipEngines的模型,拖动到场景中的SpaceShip模型里面,设置好大小和材质关联,效果如图:
![喷射效果制作](http://upload-images.jianshu.io/upload_images/3353184-4de7afc8fe8b0c6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
现在让我们用DoTween来模拟飞行喷射粒子动画,在场景中的SpaceShip里找到Ship_1_Engines 1物体,添加DoTweenAnimation组件,输入合适的参数,如下图:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3353184-803953d748f6bdcd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
最后我们在Prefab文件夹下新建一个Prefab命名为SpaceShipController,将场景里的SpaceShipController拖动到这个预置物完成飞机的制作。
****
#制作圆环敌人
在GameWorld这个GameObject里,新建一个GameObject命名为RingController,然后在素材包里面找到圆环的模型,拖动到RingEnemy里作为子物体调整好合适的大小,将Rotation的Y值变为180度,然后在Scripts文件夹里新建一个脚本命名为RingController,拖动到RingController物体处,输入以下代码:
using UnityEngine;
using System.Collections;
public class RingController : MonoBehaviour
{
//移动速度
public float Speed = 20f;
void Start()
{
}
void Update()
{
Move();
}
/// <summary>
/// 移动逻辑
/// </summary>
void Move()
{
transform.Translate(Vector3.forward * Speed * Time.deltaTime);
if (transform.localPosition.z < -1f)
{
Destroy(gameObject);
}
}
}```
这段代码的作用是用来控制圆环的,程序会控制它不断向前移动,如图:
最后在Prefab文件夹下,新建一个Prefab,命名为RingController,将场景里的RingController拖动到这个预置物,删掉场景中的RingController,后面我们要动态创建圆环。
完善游戏框架
制作好游戏模型的预制物后,我们开始通过程序动态创建圆环。在Scripts文件夹右键,新建SimpleSpawn脚本,输入以下代码:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class SimpleSpwan : MonoBehaviour
{
//敌人Prefab列表
public List<GameObject> monsterList = new List<GameObject>();
//敌人生成的Delegate事件
public delegate void CreateObjectDelegate(GameObject spawnObject);
public CreateObjectDelegate CreateObjectEvent;
//生成的间隔时间
public float spawnTimer;
//生成的最大数量
public int limitSpawn = 1;
//记录复制出来的敌人列表
private GameObject[] spawnList = new GameObject[] { };
//计数生成数量
private int coutnSpawn = 0;
//容器
private Transform _container;
void Start()
{
spawnList = new GameObject[limitSpawn];
_container = transform;
}
public void DoSpawn()
{
InvokeRepeating("Spawn", 0.5f, spawnTimer);
}
/// <summary>
/// 生成怪物
/// </summary>
public void Spawn()
{
for (int i = 0; i < spawnList.Length; i++)
{
if (spawnList[i] == null)
{
GameObject monsterSpawn = (GameObject)GameObject.Instantiate(monsterList[Random.Range(0, monsterList.Count)]);
monsterSpawn.transform.SetParent(_container, false);
SpwanObject ss = monsterSpawn.AddComponent<SpwanObject>();
ss.spawnerOwner = gameObject;
spawnList[i] = monsterSpawn;
coutnSpawn++;
if (coutnSpawn >= limitSpawn)
{
//生成敌人超过指定数量,停止执行函数
CancelInvoke("Spawn");
}
if (CreateObjectEvent != null)
{
CreateObjectEvent(monsterSpawn);
}
return;
}
}
}
/// <summary>
/// 侦听物体Destroy事件
/// </summary>
/// <param name="spwanObject"></param>
public void OnSpawnerObjectDestory(GameObject spwanObject)
{
for (int i = 0; i < spawnList.Length; i++)
{
if (spawnList[i] == spwanObject)
{
spawnList[i] = null;
//敌人死亡后,通知事件,如果已经是最大值,那么就回复时间继续生成
if (coutnSpawn >= limitSpawn)
{
Debug.Log("less then limitspawn , restart Invoke");
InvokeRepeating("Spawn", spawnTimer, spawnTimer);
}
coutnSpawn--;
return;
}
}
}
/// <summary>
/// 停止所有生成并清空
/// </summary>
public void DespawnAllAndStop()
{
for (int i = 0; i < spawnList.Length; i++)
{
if (spawnList[i] != null)
{
Destroy(spawnList[i]);
spawnList[i] = null;
}
coutnSpawn = 0;
CancelInvoke("Spawn");
}
}
/// <summary>
/// 暂停生成
/// </summary>
public void Stop()
{
coutnSpawn = 0;
CancelInvoke("Spawn");
}
}
这段代码作用是通过计时器控制程序每隔指定秒数生成一个圆环物体。核心代码在public void Spawn()
,通过检测spawnList的元素是否有数据,如果没有数据就开始生成一个新的敌人。
接着继续在Scripts文件夹右键,新建SimpleSpawnObject脚本,输入以下代码:
using UnityEngine;
using System.Collections;
public class SpwanObject : MonoBehaviour
{
//物体所有者
public GameObject spawnerOwner;
void OnDestroy()
{
//物体Destroy后发送事件给SimpleSpwan
if (spawnerOwner != null)
{
spawnerOwner.SendMessage("OnSpawnerObjectDestory", gameObject, SendMessageOptions.DontRequireReceiver);
}
}
}```
这段代码是在物体Destory后,发送消息通知SimpleSpawn脚本删除该物体的引用。
最后,我们将SimpleSpawn挂到ImageTarget里的GameWorld物体里,给SimpleSpawn的monsterList的size改为1,然后将RingController预置物拖到属性里,将SpawnTimer设置为3秒,LimitSpawn设置为5,如图:
![设置SimpleSpawn属性](http://upload-images.jianshu.io/upload_images/3353184-a73a4c863a783077.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
那么我们如何控制这个SimpleSpawn生成物体呢?在场景里右键建立一个GameObject命名为Controllers,然后在Controllers右键再建立一个GameObject命名为GameController。然后再Scripts文件夹新建一个GameController脚本,拖动到场景里的GameController物体处,输入以下代码:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class GameController : MonoBehaviour
{
//飞机控制器
public SpaceShipController _ShipController;
//生成物体系统
public SimpleSpwan SpwanSystem;
//物体的线路位置
public List<float> ItemPostion = new List<float>();
void Start()
{
Init();
}
/// <summary>
/// 游戏初始化
/// </summary>
void Init()
{
SpwanSystem.DoSpawn();
//SpwanSystem每生成一个物体,都会发送通知,并且返回这个物体
SpwanSystem.CreateObjectEvent = (GameObject spawnObject) =>
{
Vector3 newpos = spawnObject.transform.localPosition;
newpos.x = ItemPostion[Random.Range(0, ItemPostion.Count)];
newpos.z = 1.2f;
spawnObject.transform.localPosition = newpos;
};
}
}
我们在属性面板处将SpaceShipController拖到SpaceShipController属性里,将GameWorld物体拖动到SpawnSystem属性里,设置ItemPostion的值为3,如图:
![设置GameController属性](http://upload-images.jianshu.io/upload_images/3353184-31c948c6ed52f382.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
发布到手机运行测试后,你会发现圆环会在前方复制,并且不停移动,但是圆环并不会自动消失,因为我们还没有给圆环限制一个范围。双击RingController脚本,我们改写一下Move方法,给Move方法增加一个移动判断,代码如下:
/// <summary>
/// 移动逻辑
/// </summary>
void Move()
{
transform.Translate(Vector3.forward * Speed * Time.deltaTime);
if (transform.localPosition.z < -1f)
{
Destroy(gameObject);
}
}
这段代码的作用是当圆环的Z轴到-1后Destory自身,在手机上发布我们会看到,圆环移动到Z轴为-1的位置后消失了,并且SimpleSpawn会重新生成新的圆环。
****
#添加碰撞
**飞机增加刚体**
给场景中的SpaceShipController物体增加BoxCollider组件和Rigidbody组件,Rigidbody组件不需要使用重力,参数调整如下图:
![设置碰撞属性](http://upload-images.jianshu.io/upload_images/3353184-1e7e9b1e2bec1198.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)将刚体调整到只有机身部分点击Apply按钮即可,当然你可以扩大刚体范围,不过这样游戏会增大难度。
**圆环增加刚体**
将Prefabs文件夹的RingController物体拖动到GameWorld里,给圆环整体增加一个BoxCollider组件,参数调整如下图:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3353184-cb4663877502ed83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
考虑到飞机可以从圆环中间穿过,所以我们需要一点技巧来制作多一个刚体,在RingController右键建立一个新的GameObject命名为CrossCollider,增加一个BoxCollider组件,移动这个BoxCollider的位置,让这个BoxCollider比圆环的BoxCollider更加靠前,这样做的目的是当飞机碰撞到CrossCollider物体后,取消圆环的刚体碰撞,让飞机可以顺利穿越,具体参数调整如下图:
![CrossCollider](http://upload-images.jianshu.io/upload_images/3353184-1826228c7539711a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
最终效果如下图:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3353184-f67773ac880545be.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
****
#未完待续占坑
#
#
#
#
#
#
#
#
****
#制作游戏UI
#发布游戏