3D游戏(3)——空间与运动

1、简答并用程序验证【建议做】

游戏对象运动的本质是什么?

游戏对象的运动过程本质上就是游戏对象的空间位置(Position)、旋转角度(Rotation)、大小(Scale)三个属性随着时间在做某种特定的变化。

请用三种方法以上方法,实现物体的抛物线运动。(如,修改Transform属性,使用向量Vector3的方法…)

方法1:

复合运动,先写一个向右运动的脚本,再写一个向下运动的脚本,最后将两个脚本同时挂载到同一个物体上。

向右运动:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveRight : MonoBehaviour
{

    public int speed = 5;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.position += speed * Vector3.right * Time.deltaTime;
    }
}

向下运动:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveDown : MonoBehaviour
{

    public float speed = 2;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.position += speed * Vector3.down * Time.deltaTime;
        speed += 0.1f;
    }
}

方法2:

直接运用Vector3来改变position,将两个脚本合并为一个。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : MonoBehaviour
{

    public int speedx = 5;
    public float speedy = 2;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        transform.position += new Vector3(speedx * Time.deltaTime, -1 * speedy * Time.deltaTime, 0);
        speedy += 0.1f;
    }
}

方法3:

使用transform.Translate,将方法2中得出来变化向量传进Translate函数里面。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : MonoBehaviour
{

    public int speedx = 5;
    public float speedy = 2;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        transform.Translate(new Vector3(speedx * Time.deltaTime, -1 * speedy * Time.deltaTime, 0));
        speedy += 0.1f;
    }
}

写一个程序,实现一个完整的太阳系, 其他星球围绕太阳的转速必须不一样,且不在一个法平面上。

首先到红动网下载太阳系的贴图素材。把太阳系各行星摆放好。

在这里插入图片描述

参照练习03-09,首先完成一个在同一法平面上的太阳系。

在这里插入图片描述

要想各行星不在一个法平面上,可以引入一个随机数,随机生成一个法向量,这样就避免了为每一个行星各自设计一个脚本。不过,由于地球还有一个卫星(月球),需要专门设计脚本,因此,总的来说,只需写两个脚本即可。

地球外的行星环绕脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Round : MonoBehaviour
{
    public Transform center;
    public int y,z,speed;
    // Start is called before the first frame update
    void Start()
    {
        y = Random.Range(-10, 10);
        z = Random.Range(-10, 10);
        speed = Random.Range(10, 100);
    }

    // Update is called once per frame
    void Update()
    {
        transform.RotateAround(center.transform.position, new Vector3(0, y, z), speed * Time.deltaTime);
    }
}

地球的环绕脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RoundOfEarth : MonoBehaviour
{
    public Transform sun;
    public Transform moon;
    public int y,z,speed;
    // Start is called before the first frame update
    void Start()
    {
        y = Random.Range(-10, 10);
        z = Random.Range(-10, 10);
        speed = Random.Range(10, 100);
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.RotateAround(sun.position, new Vector3(0, y, z), speed * Time.deltaTime);
        moon.transform.RotateAround(this.transform.position, new Vector3(0, y, z), 20 * speed * Time.deltaTime);
    }
}

最终把各自的脚本挂载到每一颗行星上面。

在这里插入图片描述

2、编程实践

阅读以下游戏脚本

Priests and Devils

Priests and Devils is a puzzle game in which you will help the Priests and Devils to cross the river within the time limit. There are 3 priests and 3 devils at one side of the river. They all want to get to the other side of this river, but there is only one boat and this boat can only carry two persons each time. And there must be one person steering the boat from one side to the other side. In the flash game, you can click on them to move them and click the go button to move the boat to the other direction. If the priests are out numbered by the devils on either side of the river, they get killed and the game is over. You can try it in many > ways. Keep all priests alive! Good luck!

play the game

游戏很简单,先送一个魔鬼上对岸(牧师和魔鬼过去,牧师回来或者两个魔鬼过去一个魔鬼回来);再送第二个魔鬼过去(此时需要两个魔鬼过去);然后两个牧师过河,牧师带一个魔鬼回去;两个牧师过对岸,最终让魔鬼把剩下的两个魔鬼运过去。

列出游戏中提及的事物(Objects)

牧师,恶魔,船,河流,两岸

用表格列出玩家动作表(规则表),注意,动作越少越好

当前状态 玩家操作 结果
船上有空位 点击靠近船一侧岸上的恶魔或牧师 对应的恶魔或牧师上船
牧师或恶魔在船上 点击船上的恶魔或牧师 对应的恶魔或牧师上靠近船的岸
其中任意一侧的恶魔的数量大于牧师的数量 游戏失败
恶魔和牧师全部到河边另一侧 游戏胜利

请将游戏中对象做成预制

下载场景

在这里插入图片描述

下载人物模型

在这里插入图片描述

将游戏对象做成预制:

在这里插入图片描述

开始编程

首先,下载好MVC结构程序的模板。代码里面就实现了简单的太阳系。

在这里插入图片描述

SSDirector和ISceneController都能够直接使用模板的,但IUserAction里面还需要新增人物以及船的移动接口,同时还多加了一个游戏状态检查接口,即:

在这里插入图片描述

随即对UserGUI里面的OnGUI函数稍作修改,新增打印游戏成功或失败的信息。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UserGUI : MonoBehaviour {

	private IUserAction action;
	public string gameMessage;

	void Start () {
		action = SSDirector.getInstance ().currentSceneController as IUserAction;
	}

	void OnGUI() {  
		float width = Screen.width / 6;  
		float height = Screen.height / 12;

		action.Check();
		GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.red;
        style.fontSize = 30;
		GUI.Label(new Rect(320,100,50,200),gameMessage,style);

		if (GUI.Button(new Rect(0, 0, width, height), "Restart")) {  
			action.Restart();  
		} 

		string paused_title = SSDirector.getInstance ().Paused ? "Resume" : "Pause!"; 
		if (GUI.Button(new Rect(width, 0, width, height), paused_title)) { 
			SSDirector.getInstance ().Paused = !SSDirector.getInstance ().Paused;
		} 
	}
}

然后就轮到最重要的部分,也就是FirstController的编写了,首先需要的是各种控制器以及当前游戏的各个状态变量:

	private LandModelController landRoleController;
	private BoatController boatRoleController;
	private RoleModelController[] roleModelControllers;
	private MoveController moveController;
	private bool isRuning;
	private int leftPriestNum;
	private int leftDevilNum;
	private int rightPriestNum;
	private int rightDevilNum;

需要修改原来的LoadResources函数,在这里只需要创建相应的Controller即可,具体的预制实例化交给相应的Controller来干。

	public void LoadResources () {
		landRoleController=new LandModelController();
		landRoleController.CreateLand();
		roleModelControllers=new RoleModelController[6];
		for(int i=0;i<6;i++){
			roleModelControllers[i]=new RoleModelController();
			roleModelControllers[i].CreateRole(i<3? true:false,i);
			roleModelControllers[i].GetRoleModel().role.transform.localPosition=landRoleController.AddRole(roleModelControllers[i].GetRoleModel());
		}
		boatRoleController=new BoatController();
		boatRoleController.CreateBoat();
		moveController=new MoveController();
		leftPriestNum=leftDevilNum=3;
		rightPriestNum=rightDevilNum=0;
		isRuning=true;
	}

同时还需要对IUserAction里定义的四个接口做实现,这些函数的实现都建立在游戏规则的基础上,而且同样地也只需调用相应的Controller即可。

	#region IUserAction implementation
	public void MoveBoat(){
		if(!isRuning||moveController.GetIsMoving()) return;
		if(boatRoleController.GetBoatModel().isRight){
			moveController.SetMove(new Vector3(3,-0.3f,-30),boatRoleController.GetBoatModel().boat);
			leftPriestNum+=boatRoleController.GetBoatModel().priestNum;
			leftDevilNum+=boatRoleController.GetBoatModel().devilNum;
			rightPriestNum-=boatRoleController.GetBoatModel().priestNum;
			rightDevilNum-=boatRoleController.GetBoatModel().devilNum;
		}
		else{
			moveController.SetMove(new Vector3(7.5f,-0.3f,-30),boatRoleController.GetBoatModel().boat);
			leftPriestNum-=boatRoleController.GetBoatModel().priestNum;
			leftDevilNum-=boatRoleController.GetBoatModel().devilNum;
			rightPriestNum+=boatRoleController.GetBoatModel().priestNum;
			rightDevilNum+=boatRoleController.GetBoatModel().devilNum;
		}
		boatRoleController.GetBoatModel().isRight=!boatRoleController.GetBoatModel().isRight;
	}
	public void MoveRole(RoleModel roleModel){
		if(!isRuning||moveController.GetIsMoving()) return;
		if(roleModel.isInBoat){
			roleModel.isRight=boatRoleController.GetBoatModel().isRight;
			moveController.SetMove(landRoleController.AddRole(roleModel),roleModel.role);
			boatRoleController.RemoveRole(roleModel);
		}
		else if(boatRoleController.GetBoatModel().isRight==roleModel.isRight){
			landRoleController.RemoveRole(roleModel);
			moveController.SetMove(boatRoleController.AddRole(roleModel),roleModel.role);
		}
	}
	public void Restart ()
	{
		landRoleController.CreateLand();
		for(int i=0;i<6;i++){
			roleModelControllers[i].CreateRole(i<3? true:false,i);
			roleModelControllers[i].GetRoleModel().role.transform.localPosition=landRoleController.AddRole(roleModelControllers[i].GetRoleModel());
		}
		boatRoleController.CreateBoat();
		leftPriestNum=leftDevilNum=3;
		rightPriestNum=rightDevilNum=0;
		isRuning=true;
		this.gameObject.GetComponent<UserGUI>().gameMessage="";
	}
	public void Check(){
		if(!isRuning) return;
		this.gameObject.GetComponent<UserGUI>().gameMessage="";
		if(rightPriestNum==3&&rightDevilNum==3){
			this.gameObject.GetComponent<UserGUI>().gameMessage="You Win!!";
			isRuning=false;
		}
		else if((leftPriestNum!=0&&leftPriestNum<leftDevilNum)||(rightPriestNum!=0&&rightPriestNum<rightDevilNum)){
			this.gameObject.GetComponent<UserGUI>().gameMessage="Game Over!!";
			isRuning=false;
		}
	}
	#endregion

实现好FirstController之后,这么一来,游戏的大抵框架也已经做好了,接下来只需实现各个Controller以及相应的点击事件即可。

需要添加点击事件的就是人物模型跟船,两者对这事件的处理有些许不同,因而在此编写一个统一接口ClickAction

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ClickAction
{
    void DealClick();
}

因而,在一个点击事件发生之后,就调用这个接口函数即可。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Click : MonoBehaviour
{

    ClickAction clickAction;
    public void setClickAction(ClickAction clickAction){
        this.clickAction=clickAction;
    }
    void OnMouseDown(){
        clickAction.DealClick();
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Move的实现完完全全就是这一节课的内容了(空间与运动),在实现了平移运动的基础上,新增一个MoveController来管理当前运动的对象,因为“船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件!”

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : MonoBehaviour
{

    public bool isMoving=false;
    public float speed=3;
    public Vector3 des;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if(transform.localPosition==des){
            isMoving=false;
            return;
        }
        isMoving=true;
        transform.localPosition=Vector3.MoveTowards(transform.localPosition,des,speed*Time.deltaTime);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveController
{

    private GameObject moveObject;
    public bool GetIsMoving(){
        return moveObject!=null&&moveObject.GetComponent<Move>().isMoving;
    }
    public void SetMove(Vector3 des,GameObject moveObject){
        Move test;
        this.moveObject=moveObject;
        if(!moveObject.TryGetComponent<Move>(out test)) moveObject.AddComponent<Move>();
        this.moveObject.GetComponent<Move>().des=des;
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

至于船/人物/地面的实现,都是大同小异的,以小船为例,除了要实现上述的点击事件处理接口和创建小船以外,还需要对人物的上船下船进行管理,至于船的模型及相应的状态,又交给下一层BoatModel来完成。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoatController : ClickAction
{

    BoatModel boatModel;
    IUserAction userAction;

    public BoatController(){
        userAction=SSDirector.getInstance().currentSceneController as IUserAction;
    }
    public void CreateBoat(){
        if(boatModel!=null) Object.DestroyImmediate(boatModel.boat);
        boatModel=new BoatModel();
        boatModel.boat.GetComponent<Click>().setClickAction(this);
    }
    public BoatModel GetBoatModel(){
        return boatModel;
    }
    public Vector3 AddRole(RoleModel roleModel){
        if(boatModel.roles[0]==null){
            boatModel.roles[0]=roleModel;
            roleModel.isInBoat=true;
            roleModel.role.transform.parent=boatModel.boat.transform;
            if(roleModel.isPriest) boatModel.priestNum++;
            else boatModel.devilNum++;
            return new Vector3(-0.2f,0.2f,0.5f);
        }
        if(boatModel.roles[1]==null){
            boatModel.roles[1]=roleModel;
            roleModel.isInBoat=true;
            roleModel.role.transform.parent=boatModel.boat.transform;
            if(roleModel.isPriest) boatModel.priestNum++;
            else boatModel.devilNum++;
            return new Vector3(-0.2f,0.2f,-0.6f);
        }
        return roleModel.role.transform.localPosition;
    }
    public void RemoveRole(RoleModel roleModel){
        roleModel.role.transform.parent=null;
        if(boatModel.roles[0]==roleModel){
            boatModel.roles[0]=null;
            if(roleModel.isPriest) boatModel.priestNum--;
            else boatModel.devilNum--;
        }
        else if(boatModel.roles[1]==roleModel){
            boatModel.roles[1]=null;
            if(roleModel.isPriest) boatModel.priestNum--;
            else boatModel.devilNum--;
        }
    }
    public void DealClick(){
        if(boatModel.roles[0]!=null||boatModel.roles[1]!=null){
            userAction.MoveBoat();
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

BoatModel也只需要对船进行实例化和初始化就好。不过需要注意的是,游戏对象需要添加BoxCollider才能够触发点击事件。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoatModel
{

    public GameObject boat;
    public RoleModel[] roles;
    public bool isRight;
    public int priestNum,devilNum;

    public BoatModel(){
        priestNum=devilNum=0;
        roles=new RoleModel[2];
        boat=GameObject.Instantiate(Resources.Load("WoodBoat", typeof(GameObject))) as GameObject;
        boat.transform.position=new Vector3(3,-0.3f,-30);
        boat.AddComponent<BoxCollider>();
        boat.AddComponent<Click>();
        boat.GetComponent<BoxCollider>().size=new Vector3(1.5f,0.6f,2.5f);
        isRight=false;
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

剩下的人物以及地面的实现也跟小船差不多,详情可看完整代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RoleModelController : ClickAction
{

    RoleModel roleModel;
    IUserAction userAction;
    public RoleModelController(){
        userAction=SSDirector.getInstance().currentSceneController as IUserAction;
    }
    public void CreateRole(bool isPriest,int tag){
        if(roleModel!=null) Object.DestroyImmediate(roleModel.role);
        roleModel=new RoleModel(isPriest,tag);
        roleModel.role.GetComponent<Click>().setClickAction(this);
    }
    public RoleModel GetRoleModel(){
        return roleModel;
    }
    public void DealClick(){
        userAction.MoveRole(roleModel);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RoleModel// : MonoBehaviour
{

    public GameObject role;
    public bool isPriest;
    public int tag;
    public bool isRight;
    public bool isInBoat;
    public Vector3 rightPos;
    public Vector3 leftPos;

    public RoleModel(bool isPriest,int tag){
        this.isPriest=isPriest;
        this.tag=tag;
        isRight=false;
        isInBoat=false;
        rightPos=new Vector3(9,0,-33.3f+tag*1.1f);
        leftPos=new Vector3(1,0,-33.3f+tag*1.1f);
        role=GameObject.Instantiate(Resources.Load(isPriest?"Priests"+tag:"Devils", typeof(GameObject))) as GameObject;
        role.transform.position=leftPos;
        role.AddComponent<Click>();
        role.AddComponent<BoxCollider>();
        role.GetComponent<BoxCollider>().size=new Vector3(0.6f,3,0.6f);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LandModelController
{

    private LandModel landModel;

    public void CreateLand(){
        if(landModel==null) landModel=new LandModel();
    }
    public LandModel GetLandModel(){
        return landModel;
    }
    public Vector3 AddRole(RoleModel roleModel){
        roleModel.role.transform.parent=this.landModel.land.transform;
        roleModel.isInBoat=false;
        if(roleModel.isRight) return roleModel.rightPos;
        else return roleModel.leftPos;
    }
    public void RemoveRole(RoleModel roleModel){

    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LandModel
{

    public GameObject land;
    public LandModel(){
        land=GameObject.Instantiate(Resources.Load("Environment", typeof(GameObject))) as GameObject;
        land.transform.position=new Vector3(0,0,0);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

在这里插入图片描述

最后感谢师兄的博客Unity实现Priests and Deivls游戏

3、思考题【选做】

使用向量与变换,实现并扩展 Tranform 提供的方法,如 Rotate、RotateAround 等

创建一个空对象挂载脚本。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class test : MonoBehaviour
{

    public Transform obj;
    void Rotate(Vector3 axis,float angle,Space relativeTo=Space.Self){
        var rot=Quaternion.AngleAxis(angle,axis);
        obj.rotation*=rot;
    }
    void RotateAround(Vector3 point,Vector3 axis,float angle){
        var rot=Quaternion.AngleAxis(angle,axis);
        obj.position=point+(obj.position-point)*rot;
        obj.rotation*=rot;
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        Rotate(Vector3.up,30*Time.deltaTime);
    }
}

按照网上的说法来看,RotateAround似乎是可以这么实现,但是obj.position那一行会报一个error CS0019: Operator '*' cannot be applied to operands of type 'Vector3' and 'Quaternion',而obj.rotation却没有这样的报错信息,这是不是说明obj.rotation并不是Vector3类型??

不过总的来说,虽然RotateAround遇到了一点问题,但是Rotate也还是可行的。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_43278234/article/details/108813178