智能巡逻兵(订阅和发布)

本文介绍了一个基于Unity 3D的游戏项目,重点在于实现一个包含多个巡逻兵的游戏场景。巡逻兵能够沿预设路径行走,并在遇到障碍物时改变方向。此外,还详细介绍了巡逻兵和玩家之间的交互逻辑,包括玩家躲避巡逻兵获取分数的机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作业要求

  • 创建一个地图和若干巡逻兵(使用动画);
  • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
  • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
  • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
  • 失去玩家目标后,继续巡逻;
  • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;

游戏运行效果

由于上传大小限制,更清晰流畅版本见:https://github.com/Huangscar/3D-homework/blob/master/week6/gameGif.gif 

代码结构UMI图

具体代码实现

动作部分:

动作基类SSAction.cs

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

public class SSAction : ScriptableObject {

	public bool enable = true;
	public bool destory = false;

	public GameObject gameObject {get; set;}
	public Transform transform {get; set;}
	public ISSActionCallback actionCallback{get; set;}


	// Use this for initialization
	public virtual void Start () {
		throw new System.NotImplementedException("Action Start Error!");
	}
	
	// Update is called once per frame
	public virtual void Update () {
		throw new System.NotImplementedException("Action Update Error!");
	}

	public virtual void FixedUpdate() {
		throw new System.NotImplementedException("Physics Action Start Error!");
	}
}

CCAction.cs

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

public class CCAction : SSAction, ISSActionCallback {


	public List<SSAction> sequence;
	public int repeat = -1;
	public int start = 0;
	// Use this for initialization
	public override void Start () {
		foreach(SSAction ac in sequence) {
			ac.gameObject = this.gameObject;
			ac.transform = this.transform;
			ac.actionCallback = this;
			ac.Start();
		}
	}
	
	// Update is called once per frame
	public override void Update () {
		if(sequence.Count == 0) {
			return;
		}
		if(start < sequence.Count) {
			sequence[start].Update();
		}
	}

	public void SSEventAction(SSAction source, SSAtionEventType events = SSAtionEventType.COMPLETED, int intParam = 0, string strParam = null, Object objParam = null) {
		source.destory = false;
		this.start++;
		if(this.start >= this.sequence.Count) {
			this.start = 0;
			if(this.repeat == 0) {
				this.destory = true;
				this.actionCallback.SSEventAction(this);
			}
		}
	}

	public static CCAction GetSSAction(List<SSAction> _sequence, int _start = 0, int _repeat = 1) {
		CCAction actions = ScriptableObject.CreateInstance<CCAction>();
		actions.sequence = _sequence;
		actions.start = _start;
		actions.repeat = _repeat;
		return actions;
	}

	private void OnDestroy() {
		this.destory = true;	
	}

	
}

站立的动作实现 IdleAction.cs

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

public class IdleAction : SSAction {

	private float time;
	private Animator animator;

	public static IdleAction GetIdleAction(float time, Animator animator) {
		IdleAction currentAction = ScriptableObject.CreateInstance<IdleAction>();
		currentAction.time = time;
		currentAction.animator = animator;
		return currentAction;
	}

	// Use this for initialization
	public override void Start () {
		animator.SetFloat("Speed", 0);
	}
	
	// Update is called once per frame
	public override void Update () {
		if(time == -1) {
			return;
		}
		time -= Time.deltaTime;
		if(time < 0) {
			this.destory = true;
			this.actionCallback.SSEventAction(this);
		}
	}
}

跑步的动作实现

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

public class RunAction : SSAction {

	private float speed;
	private Transform target;
	private Animator animator;

	public static RunAction GetRunAction(Transform target, float speed, Animator animator) {
		RunAction run = ScriptableObject.CreateInstance<RunAction>();
		run.speed = speed;
		run.target = target;
		run.animator = animator;
		return run;
	}

	// Use this for initialization
	public override void Start () {
		animator.SetFloat("Speed", 1);
	}
	
	// Update is called once per frame
	public override void Update () {
		Quaternion rotation = Quaternion.LookRotation(target.position - transform.position);
		if(transform.rotation != rotation) {
			transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * speed * 5);
		}
		this.transform.position = Vector3.MoveTowards(this.transform.position, target.position, speed * Time.deltaTime);
		if(Vector3.Distance(this.transform.position, target.position) <0.5) {
			this.destory = true;
			this.actionCallback.SSEventAction(this);
		}
	}
}

走路的动作实现

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

public class WalkAction : SSAction {

	private float speed;
	private Vector3 target;
	private Animator animator;

	public static WalkAction GetWalkAction(Vector3 target, float speed, Animator animator) {
		WalkAction walk = ScriptableObject.CreateInstance<WalkAction>();
		walk.speed = speed;
		walk.target = target;
		walk.animator = animator;
		return walk;
	}

	// Use this for initialization
	public override void Start () {
		animator.SetFloat("Speed", 0.5f);
	}
	
	// Update is called once per frame
	public override void Update () {
		Quaternion rotation = Quaternion.LookRotation(target - transform.position);
		if(transform.rotation != rotation) {
			transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * speed * 5);
		}
		this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed*Time.deltaTime);
		if(this.transform.position == target) {
			this.destory = true;
			this.actionCallback.SSEventAction(this);
		}
	}
}

SSActionManager.cs

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

public class SSActionManager : MonoBehaviour {

	private Dictionary <int, SSAction> dictionary = new Dictionary<int, SSAction>();
	private List<SSAction> waitingAddAction = new List<SSAction>();
	private List<int> waitingDelete = new List<int>();

	// Use this for initialization
	protected void Start () {
		
	}
	
	// Update is called once per frame
	protected void Update () {
		foreach(SSAction ac in waitingAddAction) {
			dictionary[ac.GetInstanceID()] = ac;
		}
		waitingAddAction.Clear();
		foreach(KeyValuePair<int, SSAction> dic in dictionary) {
			SSAction ac = dic.Value;
			if(ac.destory) {
				waitingDelete.Add(ac.GetInstanceID());
			}
			else if(ac.enable) {
				ac.Update();
			}
		}
		foreach (int id in waitingDelete) {
			SSAction ac = dictionary[id];
			dictionary.Remove(id);
			DestroyObject(ac);
		}
		waitingDelete.Clear();
	}

	public void runAction(GameObject gameObject, SSAction action, ISSActionCallback actionCallback) {
		action.gameObject = gameObject;
		action.transform = gameObject.transform;
		action.actionCallback = actionCallback;
		waitingAddAction.Add(action);
		action.Start();
	}
}

巡逻兵的动作事件

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

[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(Rigidbody))]

public class PatrolUI : SSActionManager, ISSActionCallback, Observer {

	public enum ActionState : int{IDLE, WALKLEFT, WALKFORWARD, WALKRIGHT, WALKBACK};

	private Animator animator;

	private SSAction sSAction;
	private ActionState actionState;

	private const float walkSpeed = 1f;
	private const float runSpeed = 3f;

	// Use this for initialization
	new void Start () {
		animator = this.gameObject.GetComponent<Animator>();
		Publish publish = Publisher.getInstance();
		publish.add(this);

		actionState = ActionState.IDLE;
		idle();
	}
	
	// Update is called once per frame
	new void Update () {
		base.Update();
		if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
                {
                    transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
                }
                if (transform.position.y != 0)
                {
                    transform.position = new Vector3(transform.position.x, 0, transform.position.z);
                }
	}

	public void SSEventAction(SSAction sSAction, SSAtionEventType events = SSAtionEventType.COMPLETED, int intParam = 0, string strParam = null, Object objParam = null) {
		actionState = actionState > ActionState.WALKBACK ? ActionState.IDLE : (ActionState)((int)actionState + 1);
		switch(actionState) {
			case ActionState.WALKLEFT: {
				walkLeft();
				break;
			}
			case ActionState.WALKRIGHT: {
				walkRight();
				break;
			}
			case ActionState.WALKFORWARD: {
				walkForward();
				break;
			}
			case ActionState.WALKBACK: {
				walkBack();
				break;
			}
			default: {
				idle();
				break;
			}
		}
	}

	public void idle() {
		sSAction = IdleAction.GetIdleAction(Random.Range(1, 1.5f), animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void walkLeft() {
		Vector3 target = Vector3.left * Random.Range(3, 5) + this.transform.position;
		sSAction = WalkAction.GetWalkAction(target, walkSpeed, animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void walkRight() {
		Vector3 target = Vector3.right * Random.Range(3, 5) + this.transform.position;
		sSAction = WalkAction.GetWalkAction(target, walkSpeed, animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void walkForward() {
		Vector3 target = Vector3.forward * Random.Range(3, 5) + this.transform.position;
		sSAction = WalkAction.GetWalkAction(target, walkSpeed, animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void walkBack() {
		Debug.Log("walkingback");
		Vector3 target = Vector3.back * Random.Range(3, 5) + this.transform.position;
		sSAction = WalkAction.GetWalkAction(target, walkSpeed, animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void turnNextDirection() {
		sSAction.destory = true;
		switch(actionState) {
			case ActionState.WALKLEFT:
				actionState = ActionState.WALKRIGHT;
				walkRight();
				break;
			case ActionState.WALKRIGHT:
				actionState = ActionState.WALKLEFT;
				walkLeft();
				break;
			case ActionState.WALKFORWARD:
				walkBack();
				Debug.Log("walkback!");
				actionState = ActionState.WALKBACK;
				
				break;
			case ActionState.WALKBACK:
				actionState = ActionState.WALKFORWARD;
				walkForward();
				break;
		}
	}

	public void getGoal(GameObject gameObject) {
		sSAction.destory = true;
		sSAction = RunAction.GetRunAction(gameObject.transform, runSpeed, animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void loseGoal() {
		sSAction.destory = true;
		idle();
	}

	public void stop() {
		sSAction.destory = true;
		sSAction = IdleAction.GetIdleAction(-1f, animator);
		this.runAction(this.gameObject, sSAction, this);
	}

	public void OnCollisionEnter(Collision collision) {
		Transform transform = collision.gameObject.transform.parent;
		if(transform != null && transform.CompareTag("Wall")) {
			turnNextDirection();
		}
	}

	private void OnTriggerEnter(Collider collider) {
		if(collider.gameObject.CompareTag("Door")) {
			Debug.Log("Door!");
			turnNextDirection();
		}
	}

	public void notified(ActorState actionState, int pos, GameObject gameObject) {
		if(actionState == ActorState.ENTER_AREA) {
			if(pos == this.gameObject.name[this.gameObject.name.Length - 1] - '0') {
				getGoal(gameObject);
			}
			else {
				loseGoal();
			}
		}
		else {
			stop();
		}
	}
}

其中,巡逻兵这里设置为遇到门会转弯,防止去到别的地方,遇到墙会转弯,有一段碰撞防止位移的代码

玩家的动作事件

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

[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]

public class ActorController : MonoBehaviour
{

    private Animator animator;
    private Rigidbody rigidbody;


    private float runSpeed = 5f;
    private bool isJump = true;

    // Use this for initialization
    void Start()
    {
        animator = GetComponent<Animator>();
        rigidbody = GetComponent<Rigidbody>();
        animator.SetBool("isRun", false);
        animator.SetBool("isJump", false);
    }

    void FixedUpdate()
    {
        if(transform.position.y >= 0.06) {
            isJump = false;
        }
        if(transform.position.y < 0.06) {
            animator.SetBool("isJump", false);
            isJump = true;
        }
        if (!animator.GetBool("isAlive"))
        {
            return;
        }
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        float translationX = z * runSpeed * Time.fixedDeltaTime;
        
        Debug.Log(translationX);
        Debug.Log(z);

        animator.SetFloat("Speed", Mathf.Max(Mathf.Abs(x), Mathf.Abs(z)));

        animator.speed = 1 + animator.GetFloat("Speed") / 3;
       
        if (z != 0)
        {

            animator.SetBool("isRun", true);
          

        }
        else {
            animator.SetBool("isRun", false);
        }
        float mousX = Input.GetAxis("Mouse X") * 5.0f;
        transform.Rotate(new Vector3(0, mousX, 0));
        transform.Translate(0, 0, translationX);
        if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
        {
            transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
        }
        if (transform.position.y != 0)
        {
            transform.position = new Vector3(transform.position.x, 0, transform.position.z);
        }
        if(Input.GetKeyDown(KeyCode.Space)) {
            if(isJump) {
                animator.SetBool("isJump", true);
                //animator.SetTrigger ("jump");
                rigidbody.AddForce (Vector3.up * 10, ForceMode.VelocityChange); 
            }
        }

       
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Area"))
        {
            Debug.Log("enter area");
            Publish publish = Publisher.getInstance();
            int patrolType = other.gameObject.name[other.gameObject.name.Length - 1] - '0';
            publish.notify(ActorState.ENTER_AREA, patrolType, this.gameObject);
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        //Debug.Log("death");
        if (collision.gameObject.CompareTag("Patrol") && animator.GetBool("isAlive"))
        {
            Debug.Log("death");
            animator.SetBool("isAlive", false);
            animator.SetTrigger("toDie");
            Publisher publisher = Publisher.getInstance();
            publisher.notify(ActorState.DEATH, 0, null);
        }
    }

    // Update is called once per frame

}

这里对player的设置包括:

1.最开始处于站立状态

2.通过“W”和“S”控制玩家前进后退,通过空格控制玩家跳跃,通过鼠标左移右移控制玩家左转右转

3.当玩家处于死亡状态则不动

4.当玩家穿过门就让分数+1,碰到巡逻兵就死亡。

5.这里进行过玩家碰撞的处理

导演场控等:

IScenceController.cs

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


public enum SSAtionEventType : int {STARTED, COMPLETED}
public interface ISceneController
{
    void LoadResources();
}

public interface ISSActionCallback {
	void SSEventAction(SSAction source, SSAtionEventType events = SSAtionEventType.COMPLETED, int intParam = 0, string strParam = null, Object objParam = null);
}

public enum ActorState {ENTER_AREA, DEATH}

public interface Publish {
    void notify(ActorState actorState, int pos, GameObject gameObject);
    void add(Observer observer);
}

public interface Observer {
    void notified(ActorState actorState, int pos, GameObject gameObject);
}

导演类 SSDirector.cs

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

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;
    }
}

场控类:

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

public class FirstController : MonoBehaviour, Observer, ISceneController {

	public Text scoreText;
	public Text centerText;

	public Camera main_camera;

	public GameObject player;

	private ScoreRecorder scoreRecorder;
	private UIController uIController;
	private ObjectFactory objectFactory;

	private float[] posX = {-5, 7, -5, 5};
	private float[] posZ = {-5, -7, 5, 5};


	// Use this for initialization
	void Start () {
		SSDirector director = SSDirector.GetInstance();
		director.CurrentScenceController = this;
		scoreRecorder = new ScoreRecorder();
		scoreRecorder.scoreText = scoreText;
		uIController = new UIController();
		uIController.centerText = centerText;
		objectFactory = Singleton<ObjectFactory>.Instance;
		Publish publish = Publisher.getInstance();
		publish.add(this);
		LoadResources();
	}

	public void LoadResources() {
		player = Instantiate(Resources.Load("prefab/Ami"), new Vector3(5.5f, 0, 3), Quaternion.Euler(new Vector3(0, 100, 0))) as GameObject;
		for(int i = 0; i < 4; i++) {
			GameObject patrol = objectFactory.setObjectPosition(new Vector3(posX[i], 0, posZ[i]), Quaternion.Euler(new Vector3(0, 100, 0)));
			patrol.name = "Patrol" + (i + 1);
		}
		
		/*offset =  player.transform.position - main_camera.transform.position;
		distance = offset.magnitude;*/
		main_camera.transform.parent = player.transform;
	}

	public void notified(ActorState actorState, int pos, GameObject gameObject) {
		if(actorState == ActorState.ENTER_AREA) {
			//Debug.Log("add 1");
			scoreRecorder.addScore(1);
		}
		else {
			uIController.loseGame();
			Debug.Log("lose game");
		}
	}
	
}

在这里实现了以下功能:

1.放置玩家、巡逻兵和分数的文本

2.通过设置相机为玩家的子类来让相机跟随

3.(在玩家通过门后)分数+1

订阅和发布:

publish接口:

public interface Publish {
    void notify(ActorState actorState, int pos, GameObject gameObject);
    void add(Observer observer);
}

observe接口:

public interface Observer {
    void notified(ActorState actorState, int pos, GameObject gameObject);
}

publisher类 Publisher.cs

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

public class Publisher : Publish {

    private delegate void ActionUpdate(ActorState state, int pos, GameObject gameObject);
    private ActionUpdate updateList;

    private static Publisher _instance;

    public static Publisher getInstance() {
        if(_instance == null) {
            _instance = new Publisher();
        }
        return _instance;
    }

    public void notify(ActorState actorState, int pos, GameObject gameObject) {
        if(updateList != null) {
            //Debug.Log("this enter area");
            updateList(actorState, pos, gameObject);
        }
    }

    public void add(Observer observer) {
        updateList += observer.notified;
    }

    public void delete(Observer observer) {
        updateList -= observer.notified;
    }
}

游戏配置

玩家:



玩家运动状态机配置:











巡逻兵设置:


墙壁设置:


这里通过将墙壁所有的Tag设为Wall,用于识别墙壁

其余设置:



相关功能实现

1.相机的人物跟随

在网上搜索相机跟随都是通过寻找玩家位置,然后移动相机到达玩家附近的相应位置来完成的,但是这种方法会导致相机无法在玩家旋转后旋转,对于判断玩家的方向很不便

后来通过参考博客,只要将相机设为玩家的子类即可。

在FirstController类里,设置变量player为加载的玩家,然后:

main_camera.transform.parent = player.transform;

2.通过鼠标左移右移旋转玩家

float mouseX = Input.GetAxis("Mouse X") * 5.0f;
transform.Rotate(new Vector3(0, mouseX, 0));

3.跳跃实现

在玩家高于跑步位置的时候将isJump设为false,处于跑步位置的时候,isJump设为true,在isJump为true的时候,按下空格,trigger isJump设为true播放跳跃动画

具体代码见前面的

4.在站立的时候不播放跑步动画

当时连好状态机的时候,发现站立的时候会播放跑步的动画,通过查资料看ppt发现是没有设置完整,具体设置见游戏配置部分

完整项目代码

完整项目代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值