作业要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个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发现是没有设置完整,具体设置见游戏配置部分