提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
开发3D游戏,比起游戏的规则玩法,我觉得更重要的是如何让玩家身临其境,代入其中。因此,我会学习前辈的代码结构,并在此基础上,尝试更多的3D视觉化的效果。
简答题传送门:简答题
asset文件:asset
参考博客:学长博客
一、任务要求
二、 任务分析
1.游戏规则
三个魔鬼与三个牧师在河的一端,只通过一艘小船到达河的对岸。小船只能承载最多两人,必须有人掌舵才能移动。如果一端的牧师少于魔鬼,牧师将被杀死,游戏失败;如果全员移动到对岸,游戏成功。
2.游戏对象
牧师,魔鬼,船,河岸,河流
3.动作表
动作 | 状态 | 结果 |
---|---|---|
点击牧师或魔鬼 | 船在对应的河岸 | 牧师或魔鬼上船 |
点击牧师或魔鬼 | 牧师或魔鬼在船上 | 牧师或魔鬼下船 |
点击船 | 小船上至少有一个人 | 小船移动 |
二、 项目展示
前面mvc架构跟游戏逻辑没什么好说的,大家大差不差,网上也一堆差不多的,而第三部分才是我个人想实现的挑战跟突破
1.mvc架构
导演 SSDirector
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
场记接口 ISceneController
public interface ISceneController //加载场景
{
void LoadResources();
}
场记 Controllor
public class Controllor : MonoBehaviour, ISceneController, IUserAction
{
public LandModel start_land; //开始陆地
public LandModel end_land; //结束陆地
public BoatModel boat; //船
private RoleModel[] roles; //角色
UserGUI user_gui;
void Start ();
public void LoadResources(); //创建水,陆地,角色,船
public void MoveBoat(); //移动船
public void MoveRole(RoleModel role); //移动角色
public void Restart();
public int Check();//检测游戏状态
}
交互观众接口 IUserAction
public interface IUserAction //用户互动会发生的事件
{
void MoveBoat(); //移动船
void Restart(); //重新开始
void MoveRole(RoleModel role); //移动角色
int Check(); //检测游戏结束
}
交互观众 UserGUI
public class UserGUI : MonoBehaviour {
private IUserAction action;
public int sign = 0;
bool isShow = false;
void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI()
{
//规则展示
if (GUI.Button(new Rect(10, 10, 60, 30), "Rule", new GUIStyle("button")))
{
if (isShow)
isShow = false;
else
isShow = true;
}
if(isShow)
{
GUI.Label(new Rect(Screen.width / 2 - 85, 10, 200, 50), "让全部牧师和恶魔都渡河");
GUI.Label(new Rect(Screen.width / 2 - 120, 30, 250, 50), "每一边恶魔数量都不能多于牧师数量");
GUI.Label(new Rect(Screen.width / 2 - 85, 50, 250, 50), "点击牧师、恶魔、船移动");
}
//游戏结束
if (sign == 1||sign == 2)
{
string say;
say = sign==1?"你输了":"你赢了";
GUI.Box (new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 100), say);
if (GUI.Button (new Rect (Screen.width / 2 - 80, Screen.height / 2, 160, 20), "重开")){
action.Restart();
sign = 0;
}
}
}
}
模型 model
通过在命名空间下进行类的定义,再引用命名空间,就可以实现所有模型的引用,而不需要写多个c#文件
namespace mygame
{
public interface ISceneController //加载场景
public interface IUserAction //用户互动会发生的事件
public class SSDirector : System.Object //导演
public class LandModel//陆地模型
public class BoatModel //船模型
public class RoleModel //人物模型
public class Move : MonoBehaviour//移动辅助类
public class Click : MonoBehaviour//点击辅助类
}
预设展示
2.游戏逻辑实现
加载
将control类直接拖拽到摄像头上,即可运行。加载过程中,我对boat的加载引用了this.transform.parent,这里是因为之后的摄像头移动需要,暂且按下不表。
public void LoadResources() //创建水,陆地,角色,船
{
GameObject water = Instantiate(Resources.Load("Water", typeof(GameObject)), Vector3.zero, Quaternion.identity) as GameObject;
water.name = "water";
start_land = new LandModel("start");
end_land = new LandModel("end");
boat = new BoatModel((Transform)this.transform.parent);
roles = new RoleModel[6];
for (int i = 0; i < 3; i++)
{
RoleModel role = new RoleModel("priest");
role.SetName("priest" + i);
role.SetPosition(start_land.GetEmptyPosition());
role.GoLand(start_land);
start_land.AddRole(role);
roles[i] = role;
}
for (int i = 0; i < 3; i++)
{
RoleModel role = new RoleModel("devil");
role.SetName("devil" + i);
role.SetPosition(start_land.GetEmptyPosition());
role.GoLand(start_land);
start_land.AddRole(role);
roles[i + 3] = role;
}
}
其中模型在加载的时候使用下列方式添加move和click这俩个类,这样的方式等价于直接将脚本拖拽到对应的游戏对象上。
move = boat.AddComponent(typeof(Move)) as Move;
click = boat.AddComponent(typeof(Click)) as Click;
游戏运行
click与move赋予了模型点击后响应的能力。
click的点击实现如下,其中两个函数都在control中实现
void OnMouseDown()
{
if (boat == null && role == null) return;
if (boat != null&&boat.GetMove()==0)
action.MoveBoat();
else if(role != null&&role.GetMove()==0)
action.MoveRole(role);
}
船跟河岸都有一个游戏对象的数组,同时也存储了对应的游戏位置,移动时进行对应的判断,让游戏对象在船和两个河岸的对应数组间移动即可。很多代码细节封装成了函数,此处不再一一赘述。
public void MoveBoat() //移动船
{
if (boat.IsEmpty() || user_gui.sign != 0) return;
boat.BoatMove();
user_gui.sign = Check();
}
public void MoveRole(RoleModel role) //移动角色
{
if (user_gui.sign != 0) return;
if (role.IsOnBoat())
{
LandModel land;
if (boat.GetBoatSign() == -1)
land = end_land;
else
land = start_land;
boat.DeleteRoleByName(role.GetName());
role.GoLand(land);
role.movetoland(land);
land.AddRole(role);
}
else
{
LandModel land = role.GetLandModel();
if (boat.GetEmptyNumber() == -1 || land.GetLandSign() != boat.GetBoatSign()) return; //船没有空位,也不是船停靠的陆地,就不上船
land.DeleteRoleByName(role.GetName());
role.GoBoat(boat);
role.movetoboat(boat);
boat.AddRole(role);
}
user_gui.sign = Check();
}
游戏结束判断
游戏状态以0运行,1失败,2获胜的数表示。
public int Check()
{
int start_priest = (start_land.GetRoleNum())[0];
int start_devil = (start_land.GetRoleNum())[1];
int end_priest = (end_land.GetRoleNum())[0];
int end_devil = (end_land.GetRoleNum())[1];
if (end_priest + end_devil == 6) //获胜
return 2;
int[] boat_role_num = boat.GetRoleNumber();
if (boat.GetBoatSign() == 1) //在开始岸和船上的角色
{
start_priest += boat_role_num[0];
start_devil += boat_role_num[1];
}
else //在结束岸和船上的角色
{
end_priest += boat_role_num[0];
end_devil += boat_role_num[1];
}
if (start_priest > 0 && start_priest < start_devil) //失败
{
return 1;
}
if (end_priest > 0 && end_priest < end_devil) //失败
{
return 1;
}
return 0; //未完成
}
这些数会返回给UserGUI判断
if (sign == 1||sign == 2)
{
string say;
say = sign==1?"你输了":"你赢了";
GUI.Box (new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 100), say);
if (GUI.Button (new Rect (Screen.width / 2 - 80, Screen.height / 2, 160, 20), "重开")){
action.Restart();
sign = 0;
}
}
3.3d效果实现
上次作业是上手unity,那这次的就是上手mvc跟学会空间系的灵活应用。
模型的布置以(0,0,0)为中心对称排布,这样的话方便了对岸跟当前河岸的位置处理,只需要取反即可。
人物布置与移动
人物的布置的话,我按照个人的审美进行错落的排布,同时把船简化成方块,这样就能只进行水平上的移动。我把上下船的动作解耦成五步:转向中间区域,移动向中间区域,转向目标,移动向目标,站定后转向。
void roleUpdate(){
if(move_sign == 1){// 移动前转向
transform.rotation = Quaternion.RotateTowards(transform.rotation, mid_qua, rotaspeed*Time.deltaTime);
if(transform.rotation==mid_qua)move_sign=2;
}
else if(move_sign == 2){//移动到中间
transform.position = Vector3.MoveTowards(transform.position, mid_position, mspeed * Time.deltaTime);
if (transform.position == mid_position)move_sign=3;
}
else if(move_sign == 3){//在中间转向
transform.rotation = Quaternion.RotateTowards(transform.rotation, end_qua, rotaspeed*Time.deltaTime);
if(transform.rotation==end_qua)move_sign=4;
}
else if(move_sign == 4){//移动到最终目的地
transform.position = Vector3.MoveTowards(transform.position, end_position, mspeed * Time.deltaTime);
if (transform.position == end_position)move_sign=5;
}
else if(move_sign == 5){//在最终目的地转向
transform.rotation = Quaternion.RotateTowards(transform.rotation, final_qua, rotaspeed*Time.deltaTime);
if(transform.rotation==final_qua)move_sign=0;
}
}
// -90前 0右 90后 180左
public void movetoland(LandModel land){
Vector3 position = land.GetEmptyPosition();
mid_position = new Vector3(position.x,transform.position.y,transform.position.z);
end_position = new Vector3(position.x,transform.position.y,position.z);
if(land.GetLandSign() == 1){//走下初始大陆上的转向方式
mid_qua = Quaternion.Euler(0,90,0);
if(position.z>transform.position.z)end_qua= Quaternion.Euler(0,0,0);
else end_qua = Quaternion.Euler(0,180,0);
final_qua = Quaternion.Euler(0,-90,0);
}
else{//走下对面大陆上的转向方式
mid_qua = Quaternion.Euler(0,-90,0);
if(position.z>transform.position.z)end_qua= Quaternion.Euler(0,0,0);
else end_qua = Quaternion.Euler(0,180,0);
final_qua = Quaternion.Euler(0,90,0);
}
kind = 1;
move_sign = 1;
}
public void movetoboat(BoatModel boat){
Vector3 position = boat.GetEmptyPosition();
mid_position = new Vector3(transform.position.x,transform.position.y,position.z);
end_position = new Vector3(position.x,transform.position.y,position.z);
if(boat.GetBoatSign() == 1){//船在初始大陆上的转向方式
if(position.z>transform.position.z)mid_qua = Quaternion.Euler(0,0,0);
else mid_qua = Quaternion.Euler(0,180,0);
end_qua = final_qua = Quaternion.Euler(0,-90,0);
}
else{//船在对面大陆上的转向方式
if(position.z>transform.position.z)mid_qua = Quaternion.Euler(0,0,0);
else mid_qua = Quaternion.Euler(0,180,0);
end_qua = final_qua = Quaternion.Euler(0,90,0);
}
kind = 1;
move_sign = 1;
}
船的移动
船的移动本身不复杂,人物通过将parent挂载在船上就可与船进行平移,难的是镜头的转换。经过多次尝试,我在(0,0,0)处放置空对象,而摄像头挂载在对象下,这样就可以通过固定角度旋转空对象,实现镜头切换的效果,同时我还调整了减慢的参数。
void boatUpdate(){
if(move_sign==1){
//渐慢的镜头旋转
maincamera.rotation = Quaternion.RotateTowards(maincamera.rotation, cameraq, rotaspeed*Time.deltaTime);
if(rotaspeed>0)rotaspeed -= 120*Time.deltaTime>rotaspeed?rotaspeed: 120*Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, end_position,mspeed * Time.deltaTime);
if(transform.position==end_position)move_sign=0;
}
}
public void boatmove(Vector3 position,Transform camera){
end_position = position;
maincamera = camera;
//摄像头翻转
if(camera.rotation==Quaternion.Euler(0,0,0))cameraq = Quaternion.Euler(180,0,180);
else cameraq = Quaternion.Euler(0,0,0);
kind = 2;
move_sign=1;
rotaspeed = 200;
mspeed = 2;
}
三、 展示效果
1.展示图
2.展示视频
牧师与魔鬼 演示