Giter VIP home page Giter VIP logo

3dgamedesignhomeworkweek15's Introduction

3DGameDesignHomeworkWeek15

preview

视频链接:https://www.bilibili.com/video/av25139467/

Tanks

【这篇博客暂时就发到github包里,近日凉凉重装系统还没有把hexo装回来。我会尽快同步到github的blog上的】

首先看一下这次的游戏都有哪些对象以及他们所需要的各种操作:

Tank

​ 无论是Play还是NPC都具有Tank的属性,而其中的公有部分就是它们的血条。因而创建一个基类Blood,之后让Player和NPC都集成它并发展它们各自不同的功能。

public class Blood : MonoBehaviour {
	private float blood;
	public float getBlood() {
		return blood;
	}
	public void setBlood(float blood0) {
		blood = blood0;
	}
}

Player

​ 如果血条降为0,则发送destroyEvent,并且在场控类中对该消息进行监听,以控制游戏的开始结束。

// Player.cs
public delegate void destroy(int result);
public static event destroy destroyEvent;
...
void Update () {
	if (getBlood() <= 0) {
		this.gameObject.SetActive(false);
		if(destroyEvent != null) {
			destroyEvent(-1);
		}
	}		
}

​ 场控一开始就要对Player的destroyEvent事件订阅,并作出将result设为1(表示胜利)的响应。

void Start () {
	...
	Player.destroyEvent += setResult; 
}; 

NPC

NPC需要控制的主要有两方面:一方面,要判断自身是否还活着(即血量是否还>0),另一方面,要追逐Player并对其进行攻击。

NavMeshAgent

参考Unity3D官网文档:https://docs.unity3d.com/Manual/nav-MoveToDestination.html,利用`SeDestination`函数将Player作为寻路`destination`即可。

void Update () {
	if(sceneController.getResult()==0) {
		target = sceneController.getPlayerPosition();
		// 如果NPC没血了,则被摧毁
		if(getBlood() <= 0.0f && recycleEvent != null) {
			recycleEvent(this.gameObject);
			sceneController.decreaseCountNPC();
		}
		else {
		// 否则专注攻击玩家
			NavMeshAgent agent = GetComponent<NavMeshAgent>();
			agent.SetDestination(target);
		}
	}
	else {
		// 游戏结束,停止寻路
		NavMeshAgent agent = GetComponent<NavMeshAgent> ();
		agent.velocity = Vector3.zero;
		agent.ResetPath();
	}
}
Coroutines

对于坦克射击的动作,并不是在游戏的一帧内就完成的,因此需要用到Coroutines.

A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.

这里还需要控制坦克发射子弹的间隔。

发射子弹的主要思路为从工厂中获得子弹,并用物理引擎Rigidbody赋予其水平速度,由于是子弹的发射,因而这里力的模式为Impulse.

力的模式总结如这篇博客

ForceMode.Force:给物体添加一个持续的力并使用其质量。

ForceMode.Acceleration::给物体添加一个持续的加速度,但是忽略其质量。

ForceMode.Impulse;:给物体添加一个瞬间的力并使用其质量

ForceMode.VelocityChange;:给物体添加一个瞬间的加速度,但是忽略其质量

IEnumerator shoot() { // 协程实现npc坦克射击
	while(sceneController.getResult()==0) {
		// 控制发射子弹间隔
		for(double i = 3; i > 0; i -= Time.deltaTime) {
			yield return null;
		}
		if (Vector3.Distance(transform.position, target) < 20) {
			Factory myFactory = Singleton<Factory>.Instance;
			GameObject bullet = myFactory.getBullet(tankType.NPC);
			bullet.transform.position = new Vector3(transform.position.x, 1.5f, transform.position.z) + transform.forward*1.5f;
			bullet.transform.forward = transform.forward;
			Rigidbody rb = bullet.GetComponent<Rigidbody>();
			rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse);
		}
	}
}

Bullet

作为挂载在子弹上的代码,需要处理的就是子弹的碰撞坦克的事件。游戏中的设定为子弹打击到物体会产生爆炸,而所有爆炸所波及到的对立物体(这里指player子弹产生的爆炸会使NPC的血条受损,同样NPC子弹产生的爆炸会使player的血条受损)都到受到损伤,因而采用API:Physics.OverlapSphere. 在这篇博客中有详细用法的介绍。

void OnCollisionEnter(Collision other) {
	Debug.Log("OnCollisionEnter");
	Factory myFactory = Singleton<Factory>.Instance;
	ParticleSystem explosion = myFactory.getPS();
	explosion.transform.position = transform.position;
	Collider[] colliders = Physics.OverlapSphere(transform.position, explosionRadius);
	for (int i = 0; i < colliders.Length; i++) {
		// 如果玩家子弹击中了NPC或者NPC子弹击中了玩家坦克,伤害才有效
		if(colliders[i].tag == "tankPlayer" && this.type == tankType.NPC || 
			colliders[i].tag == "tankNPC" && this.type == tankType.PLAYER ) {
			//Debug.Log("对" + colliders[i].tag + "造成伤害");
			float distance = Vector3.Distance(colliders[i].transform.position, transform.position);
			float hurt = 100f/distance; // 距离越远伤害越小;
			float current = colliders[i].GetComponent<Blood>().getBlood();
			colliders[i].GetComponent<Blood>().setBlood(current - hurt);
		}
	}
	explosion.Play();
	if(this.gameObject.activeSelf) {
		myFactory.recycleBullet(this.gameObject);
	}
}

IUserGUI

​ 与用户交互的界面主要分为两个部分:

用户键盘输入操纵游戏

相应的接口在FirstController中实现:

void Update () {
	if(action.getResult() == 0) {
		// 读取键盘输入,根据键盘输入调用相应函数
		if(Input.GetKey(KeyCode.W)) {
			action.moveForward();
		}
		else if(Input.GetKey(KeyCode.S)) {
			action.moveBack();
		}
		else {
			action.noMove();
		}
		if(Input.GetKeyDown(KeyCode.Space)) {
			action.shoot();
		}
		float deltaX = 1f;
		if(Input.GetKey(KeyCode.A)) {
			action.turn((-1)*deltaX);
		}
		else if(Input.GetKey(KeyCode.D)) {
			action.turn(deltaX);
		}
		else {
			action.noTurn();
		}
	}
}

信息输出告知结果

这里的内容第一节课就学过了,代码如下:

void OnGUI() {
	labelStyle = new GUIStyle("label");
	labelStyle.alignment = TextAnchor.MiddleCenter;
	labelStyle.fontSize = Screen.height/15;
	GUI.color = Color.black;
	if(action.getResult() == 1) {
		Debug.Log("YOU WIN!");
		GUI.Label(new Rect(Screen.width/2 - Screen.width/8,Screen.height/2 - Screen.height/8,Screen.width/4,Screen.height/4), "YOU WIN!",labelStyle);
	}
	else if(action.getResult() == -1) {
		Debug.Log("Game Over!");
		GUI.Label(new Rect(Screen.width/2 - Screen.width/8,Screen.height/2 - Screen.height/8,Screen.width/4,Screen.height/4), "Game Over!",labelStyle);
	}
}

Director

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

public class Director : System.Object {
	private static Director instance;
	public FirstController currentSceneController {get; set;}
	public static Director getDirector () {
		if(instance == null) {
			instance = new Director();
		}
		return instance;
	}
	
}

Factory

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

public enum tankType: int {PLAYER, NPC}
public class Factory : MonoBehaviour {
	public GameObject player;
	public GameObject npc;
	public GameObject bullet;
	public ParticleSystem ps;

	private Dictionary<int, GameObject> tanks;
    private Dictionary<int, GameObject> freeTanks;
    private Dictionary<int, GameObject> bullets;
    private Dictionary<int, GameObject> freeBullets;
	
    private List<ParticleSystem> psQueue;
	// Use this for initialization
	void Awake () {
		tanks = new Dictionary<int, GameObject>();
		freeTanks = new Dictionary<int, GameObject>();
		bullets = new Dictionary<int, GameObject>();
		freeBullets = new Dictionary<int, GameObject>();
		psQueue = new List<ParticleSystem>();
	}

	void Start() {
		NPC.recycleEvent += recycleTank;
	}
	
	public GameObject getPlayer() {
		return player;
	}

	public GameObject getTank() {
		// 如果没有坦克可以使用了,则新生成坦克
		if(freeTanks.Count == 0) {
			// 新生成一个坦克的血条
			GameObject newTank = Instantiate(Resources.Load<GameObject>("Prefabs/npc"), new Vector3(0,0,0),  Quaternion.identity) as GameObject;
			// 将坦克加入坦克生成队列
			tanks.Add(newTank.GetInstanceID(), newTank);
			// 新生成的坦克随机出现在地图上
			newTank.transform.position = new Vector3(Random.Range(-20,20), 0, Random.Range(-20,20));
			return newTank;
		}
		// 否则仍然有可用的坦克,则将工厂中的坦克激活
		foreach (KeyValuePair<int, GameObject> pair in freeTanks) {
			pair.Value.SetActive(true);
			freeTanks.Remove(pair.Key);
			tanks.Add(pair.Key, pair.Value);
			pair.Value.transform.position = new Vector3(Random.Range(-20,20), 0, Random.Range(-20,20));
			return pair.Value;
		}
		return null;
	}
	
	public GameObject getBullet(tankType type) {
		if(freeBullets.Count == 0) {
			GameObject newBullet = Instantiate(Resources.Load<GameObject>("Prefabs/Shell"), new Vector3(0,0,0),  Quaternion.identity) as GameObject;
			// npc 和 player 发射子弹的方式不同
			newBullet.GetComponent<Bullet>().setTankType(type);
			newBullet.tag = "bullet";
			bullets.Add(newBullet.GetInstanceID(), newBullet);
			return newBullet;
		}
		foreach (KeyValuePair<int, GameObject> pair in freeBullets) {
			pair.Value.SetActive(true);
			pair.Value.GetComponent<Bullet>().setTankType(type);
			freeBullets.Remove(pair.Key);
			bullets.Add(pair.Key, pair.Value); 
			return pair.Value;
		}
		return null;
	}

	public ParticleSystem getPS() {
		for (int i = 0; i < psQueue.Count; i++) {
			if(!psQueue[i].isPlaying) {
				return psQueue[i];
			}
		}
		ParticleSystem newPS = Instantiate<ParticleSystem>(ps);
		psQueue.Add(newPS);
		return newPS;
	}

	/*public void destroyEvent(GameObject tank) {
		tanks.Remove(tank.GetInstanceID());
		freeTanks.Add(tank.GetInstanceID(), tank);
		tank.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
		tank.SetActive(false);
	}
*/
	public void recycleTank(GameObject tank) {
        tanks.Remove(tank.GetInstanceID());
        freeTanks.Add(tank.GetInstanceID(), tank);
        tank.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
        tank.SetActive(false);
    }


	public void recycleBullet(GameObject bullet) {
		//Debug.Log("recycleBullet");
		bullets.Remove(bullet.GetInstanceID());
		freeBullets.Add(bullet.GetInstanceID(), bullet);
		bullet.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
		bullet.SetActive(false);
		//Debug.Log("recycleBullet" + freeBullets.Count);
	}
}

控制主摄像机

将脚本挂载到主摄像机上,即可跟随player移动。

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

public class CameraController : MonoBehaviour {
	public GameObject player;
	private Vector3 offset;//it's a vector from player to camera
	//offset can be obtained by transform(camera) - player
	// Use this for initialization
	void Start () {
		offset = transform.position - player.transform.position;
	}
	
	// Update is called once per frame
	void LateUpdate () {
		transform.position = player.transform.position + offset;
	}
}

控制血条

将脚本挂载到player上即可。

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

public class BloodController : MonoBehaviour
{
    private float hScrollbarValue;
    public Slider healthSlider;
    private float fullBlood;
    void Start() {
    	fullBlood = 500f;

    }
    void OnGUI () {
    	GUI.color = Color.red;
        //hScrollbarValue = GUI.HorizontalScrollbar (new Rect (0, 0, 100, 30), 0.0f, this.gameObject.transform.parent.GetComponent<Blood>().getBlood()*0.02f, 0.0f, 10.0f);
    	healthSlider.value = this.gameObject.GetComponent<Blood>().getBlood()/fullBlood;
    	//Debug.Log(this.gameObject.GetComponent<Blood>().getBlood());
    }
}

FirstController

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

public class FirstController : MonoBehaviour, IUserAction {
	public GameObject playerTank;
	private bool gameOver = false;
	private int result = 0; // 0->onGame, 1->win, -1->lose;

	private int countNPC = 5;
	private Factory myFactory;
	private float speed = 30f;

	float lastX;
	float lastY;

	void Awake() {
		Director director = Director.getDirector();
		director.currentSceneController = this;
		myFactory = Singleton<Factory>.Instance;
		playerTank = myFactory.getPlayer();
	}
	// Use this for initialization
	void Start () {
		for(int i = 0; i < countNPC; i++) { // 从工厂获得一定数目的NPC
			myFactory.getTank();
		}
		// 订阅Player发布的destroy事件,并响应
		Player.destroyEvent += setResult; 
	}

	void Update() {
		if(countNPC <= 0) {
			setResult(1);
		}
	}

	public void moveForward() {
		playerTank.GetComponent<Rigidbody>().velocity = playerTank.transform.forward*speed;
	}

	public void moveBack() {
		playerTank.GetComponent<Rigidbody>().velocity = playerTank.transform.forward * (-1)*speed;
	}

	public void turn(float deltaX) {
		float x = playerTank.transform.localEulerAngles.y + deltaX*5;
		float y = playerTank.transform.localEulerAngles.x;
		lastX = x;
		lastY = y;
		playerTank.transform.localEulerAngles = new Vector3(y,x,0);
	}

	public void noTurn() {
		playerTank.transform.localEulerAngles = new Vector3(lastY,lastX,0);
	}

	public void noMove() {
		playerTank.GetComponent<Rigidbody>().velocity = Vector3.zero;
	}

	public void shoot() {
		GameObject bullet = myFactory.getBullet(tankType.PLAYER);
		bullet.transform.position = new Vector3(playerTank.transform.position.x, 1.5f, playerTank.transform.position.z)
			+ playerTank.transform.forward * 1.5f; // 坦克的位置 + 子弹相对坦克的位置
		bullet.transform.forward = playerTank.transform.forward;
		Rigidbody rb = bullet.GetComponent<Rigidbody>();
		rb.AddForce(bullet.transform.forward * speed * 10, ForceMode.Impulse);
	}

	public Vector3 getPlayerPosition() {
		return playerTank.transform.position;
	}

	public void setResult(int result) {
		this.result = result;
	}

	public int getResult() {
		return result;
	} 
	public void decreaseCountNPC() {
		countNPC--;
	}
}

常见问题的处理方法

碰撞问题

坦克之间不会互相穿越

​ 修改tank的BoxCollider组件的size(有点像形成了一个盒装结界,就没有办法因为靠得太近发生碰撞啦)。

BoxCollider

碰撞不产生莫名的旋转

​ 之前一直用代码控制transform的rotation保持不变,后来发现Rigidbody已经为我们做好了组件。把Constraints的freeze Rotation打勾即可,包括如果不希望发生位移的话,在freeze position打勾就好了。

Rigidbody

GetKey和GetKeyDown的区别

顾名思义,一个是按着,一个是按下。作为一个玩家,在控制旋转操作的时候,一开始用的keyDown,但会发现如果想让坦克旋转,则必须一下一下敲击键盘,而更人性化的方法明显是长按,因而这里使用GetKey更为合理。

挂载脚本的顺序

​大坑!!制作过程中遇到虽然Bullet每次攻击都调用了Blood脚本的SetBlood函数,但Debug.Log仍然发现玩家和NPC的血条一直是初始值。

NavMesh相关学习笔记

启动面板

​ windows - navigation

寻路地形

​ 打勾Navigation Static - 使当前物体作为寻路功能的一部分

​ Object - 当前物体

​ Bake - 全局

各种具体功能的实现

障碍物绕行

​ 将障碍物作为Not Walkable

爬楼梯/跳跃(OffMeshLink)

爬楼梯

​ 楼梯开始/结束位置放置两个顶点startPoint, endPoint,可以用empty GameObject制作。

​ 选择楼梯,添加OffMeshLink,拖入startPointendPoint.

跳跃

​ 跳下:Navigatuon - Bake - Drop Height 掉落高度

​ 跳过:... - Jump Distence 横向跳跃距离

​ 打勾 Navigation 的 OffMeshLink

3dgamedesignhomeworkweek15's People

Contributors

yuandi-sherry avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.