Hit And Run Practice
Introduction
在RPG游戏中,控制角色对敌方单位进行攻击这样的战斗系统几乎是必备的,而在战斗系统中,根据玩家的输入进行角色的自动寻路则也是系统的基础功能。这个DEMO主要介绍了一个简单战斗表现的实现和利用NavMesh进行自动寻路。
Battle System
战斗系统的实现思路步骤如下:
* 在Unity3D中导入一个人物模型,并创建一个可供人物活动的Terrain
* 调整摄像机至正确的视角(可以始终处于人物背后也可以45度俯角跟随)
* 创建攻击动画与伤害显示
* 创建血条
* 逻辑脚本编写(包括攻击方向与距离求解,攻击物体判定)
* 运行测试
其中,对于具体的攻击在每一帧中的实现逻辑思路如下(攻击动作由鼠标与攻击按键共同完成,鼠标位置代表攻击方向,按键则给出攻击指令):
* 获取鼠标位置Input.MousePosition
* 由当前鼠标位置与角色位置求出攻击方向向量并归一化
* 由角色位置向攻击方向向量做出一条长度为攻击距离的线段,求解与线段相交的物体集合
* 遍历物体集合,对其中的可攻击对象执行HIT方法
* 播放角色攻击动画,如攻击方向与角色朝向不一致则先进行角色转身
角色控制脚本主要Code如下:
using UnityEngine;
using UnityEngine.AI;
using System;
using System.Collections;
using System.Collections.Generic;
#pragma warning disable
public class ASCLBasicController : MonoBehaviour
{
Animator animator;
public Collider floorPlane;//in this demonstration this is set manually, the Retail Ability system has methods for dealing with this automatically via data structures for environments
public Collider attackPlane;
public Enemy[] enemies;//this array is filled during START by searching for prefabs that have the enemy script attached to them
public Transform hitReport;//this is for a text mesh object that tells us what damage we did...we need to know where this instance is so we can instantiate off of it
public Transform particleHit;//this is for a particle emmiter that shows us a hit...we need to know where this instance is so we can instantiate off of it
//the retail ability system will have this "Inside" an abilities class structual data
public List<Ability> abilities = new List<Ability>();//not really actual abilities as yet, HERE they are only attacks...the retail Ability System package will have actual ones
int ahc=1; //ability hit counter, combo attacks have more than one hit, this variable keeps track of how many hits we have used in update
public bool hitCheck;//<<< IMPORTANT mecanim tells us to perform a hit check at a specific point in attack animations by settting this to TRUE
public int WeaponState=0;//unarmed, 1H, 2H, bow, dual, pistol, rifle, spear and ss(sword and shield)
public bool wasAttacking;// we need this so we can take lock the direction we are facing during attacks, mecanim sometimes moves past the target which would flip the character around wildly
public Renderer movementTarget;
Transform destFloor;
float rotateSpeed = 20.0f; //used to smooth out turning
public Vector3 attackPos;
public Vector3 lookAtPos;
float gravity = -0.3f;//unused in this demonstration
float fallspeed = 0.0f;
NavMeshAgent mr;
bool isArrived;
public bool rightButtonDown=false;//we use this to "skip out" of consecutive right mouse down input...
// Use this for initialization
void Start ()
{
animator = GetComponentInChildren<Animator>();//need this...
movementTarget.transform.position = transform.position;//initializing our movement target as our current position
movementTarget.enabled = true;
lookAtPos = transform.position+transform.forward;
enemies = Transform.FindObjectsOfType(typeof(Enemy))as Enemy[];//find all the instances of the enemy script which are attached to the targets
mr = GetComponent<NavMeshAgent>();
isArrived = true;
}
// Update is called once per frame
void LateUpdate ()
{
if(hitCheck)//hitCheck is a boolean variable, it gets set by mecanim attack states, if mecanim set it...then we need to do hit checks right now
{
if(ahc<1) ahc=1;//we may have a "double pulse" coming from Mecanim so...
AbilityCollision abilColl = new AbilityCollision();
abilColl = abilities[WeaponState].collChecks[abilities[WeaponState].collChecks.Count-ahc];
if(abilColl.type==0)
{
//ANGLE RANGE which can be used for any angle/range including radial attacks
for(int i =0;i<enemies.Length;i++)//loop throught the enemies
{
CheckForHit(enemies[i], abilColl);//ahc= ability hit counter, which is used for indexing a a one tow three punch combo for example...
}
ahc-=1;// some abilities have multiple checks, so when we use an ability, we set ahc to the number of hits in the ability (combo punches for example)
}
else if(abilColl.type==1)
{
//Missiles
//are a special type, my enemies are its enemies, my damage is its damage, it needs to know who I am, and which of my abilities used it this time
Transform tm = (Transform) Instantiate(abilColl.missile , abilColl.missile.position,abilColl.missile.rotation);
tm.gameObject.SetActive(true);
Missile missile = tm.GetComponent<Missile>();
missile.speed = abilColl.speed;
missile.damage = abilColl.damage;
missile.enemies = enemies;
missile.abc = this;
}
if(abilColl.type==2)
{
//Beams/Bullets
Transform tm = (Transform) Instantiate(abilColl.missile , abilColl.missile.position,abilColl.missile.rotation);
tm.gameObject.SetActive(true);
RayShot rayshot = tm.GetComponent<RayShot>();
rayshot.damage = abilColl.damage;
rayshot.enemies = enemies;
Vector3 tempPos = attackPos;
tempPos.y = abilColl.missile.position.y;
Vector3 tempdelta = Vector3.Normalize(tempPos - abilColl.missile.position); //this is the actual vector from the source point to the attack point normalized
rayshot.endPos = tempdelta* abilColl.range;
rayshot.abc = this;
}
hitCheck = false;// we are done checking, reset the hitCheck bool
}
RaycastHit hit;// RayCastHits hold very useful info such as hitnormal and location
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);//get a ray that goes from the camera -> "THROUGH" the mouse pointer - > and out into the scene
if ( ! Input.GetKey(KeyCode.LeftAlt))//if we are not using the ALT key(camera control)...
{
// floor goal
if(Input.GetMouseButton(0))//is the left mouse button being clicked?
{
if(floorPlane.Raycast(ray, out hit, 500.0f)) //check to see if that ray hits our "floor"
{
movementTarget.transform.position = hit.point;//mark it where it hit
movementTarget.enabled=true;
mr.SetDestination(hit.point);
lookAtPos = hit.point;
lookAtPos.y = transform.position.y;
wasAttacking = false;//we're moving now, not attacking
}
}
}
switch(Input.inputString)//get keyboard input, probably not a good idea to use strings here...Garbage collection problems with regards to local string usage are known to happen
{ //the garbage collection memory problem arises from local alloction of memory, and not freeing it up efficiently
case "p":
animator.SetTrigger("Pain");//the animator controller will detect the trigger pain and play the pain animation
break;
case "a":
animator.SetInteger("Death", 1);//the animator controller will detect death=1 and play DeathA
break;
case "b":
animator.SetInteger("Death", 2);//the animator controller will detect death=2 and play DeathB
break;
case "c":
animator.SetInteger("Death", 3);//the animator controller will detect death=3 and play DeathC
break;
case "n":
animator.SetBool("NonCombat", true);//the animator controller will detect this non combat bool, and go into a non combat state "in" this weaponstate
break;
default:
break;
}
animator.SetInteger("WeaponState", WeaponState);// probably would be better to check for change rather than bashing the value in like this
if ( ! Input.GetKey(KeyCode.LeftAlt)) // if we're changing camera transforms, do not use "USE"
{
if(Input.GetMouseButton(1) || Input.GetKey(KeyCode.K))// are we using the right button?
{
//Debug.Log ("11111111111111");
if(rightButtonDown != true)// was it previously down? if so we are already using "USE" bailout (we don't want to repeat attacks 800 times a second...just once per press please
{
attackPlane.enabled=true;
if(attackPlane.Raycast(ray, out hit, 500.0f))
{
movementTarget.transform.position = transform.position; //we are attacking so lock our position to where we are
attackPos = hit.point;// establish the point that we hit with the mouse
attackPos.y = transform.position.y;//use our height for the LOOKAT function, so we stay level and dont lean the character in weird angles
Vector3 attackDelta = attackPos - transform.position;//we need the Vector delta which is an un-normalized direction vector
lookAtPos = attackPos;
animator.SetTrigger("Use");//tell mecanim to do the attack animation(trigger)
ahc = abilities[WeaponState].collChecks.Count;//ahc=ability hit counter, used for animations that have multiple hits like combos
animator.SetBool("Idling", true);//stop moving
rightButtonDown = true;//right button was not down before, mark it as down so we don't attack 800 frames a second
wasAttacking =true;//some mecanims will actually move us past the target, so we want to keep looking in one direction instead of spinning wildly around the target
}
attackPlane.enabled=false;
}
}
}
if (Input.GetMouseButtonUp(1) || Input.GetKeyUp(KeyCode.K))//ok, we can clear the right mouse button and use it for the next attack
{
if (rightButtonDown == true)
{
rightButtonDown = false;
}
}
Debug.DrawLine ((movementTarget.transform.position + transform.up*2), movementTarget.transform.position);//useful for visuals in editor
//We need to handle elevation for mecanim here...we will be doing our ground check right now
//GROUND check
ray.direction = transform.up*-1;
ray.origin = transform.position+transform.up;
//need two casts...one for velocity ahead of us
//and one for where we are
//we need the time from the last frame, as well as the distance traveled since that frame
//RAMPS,Coming down from Jumps and Falling
if(floorPlane.Raycast(ray, out hit, 1.0f))
{
//always hit if we are going up
if(hit.point.y>(transform.position.y + 0.02f))
{
transform.position=hit.point;
lookAtPos.y = transform.position.y;
fallspeed=0.0f;
}
}
else if(floorPlane.Raycast(ray, out hit, 1.2f))
{
//lower hit check for going down ramps specifically
if(hit.point.y < (transform.position.y - 0.02f))
{
transform.position=hit.point;
lookAtPos.y = transform.position.y;
fallspeed=0.0f;
}
}
else
{//Falling
transform.parent=null;
lookAtPos=transform.position;
movementTarget.transform.position=transform.position;
movementTarget.transform.parent=null;
fallspeed+=0.3f;
Vector3 v = new Vector3(0.0f,fallspeed*Time.deltaTime,0.0f);
transform.position-=v;
}
Debug.DrawLine ((movementTarget.transform.position + transform.up*2), lookAtPos+ transform.up*2);//useful for visuals in editor
if(transform.parent == floorPlane.transform)
{
lookAtPos = movementTarget.transform.position;
}
lookAtPos.y = transform.position.y;
Quaternion tempRot = transform.rotation; //save current rotation
transform.LookAt(lookAtPos);
Quaternion hitRot = transform.rotation; // store the new rotation
// now we slerp orientation
transform.rotation = Quaternion.Slerp(tempRot, hitRot, Time.deltaTime * rotateSpeed);
if(Vector3.Distance(movementTarget.transform.position,transform.position)>0.5f)
{
animator.SetBool("Idling", false);
}
else
{
animator.SetBool("Idling", true);
movementTarget.enabled = false;
}
}
void OnGUI()
{
string tempString = "LMB=move RMB=attack p=pain abc=deaths";
GUI.Label (new Rect (10, 5,1000, 20), tempString);
}
void CheckForHit(Enemy en, AbilityCollision ac)
{
//AngleRanged
if(ac.type==0)
{
float angle=ac.angle/2;
Vector3 tDelta = en.gameObject.transform.position - transform.position;
float tAngle = Vector3.Angle(transform.forward,tDelta);
if (tAngle< 0) tAngle*=-1;
if (tAngle<angle)
{
if(Vector3.Distance(transform.position, en.gameObject.transform.position)<ac.range)
{
//we have a hit
//AngleRanged
Transform tm = (Transform) Instantiate(hitReport , (en.gameObject.transform.position + new Vector3(0.0f,1.6f,0.0f)),Quaternion.identity);
tm.gameObject.SetActive(true);
Hit tmHit = tm.GetComponent<Hit>();
tmHit.text = ac.damage.ToString();
Transform ph = (Transform) Instantiate(particleHit , (en.gameObject.transform.position + new Vector3(0.0f,1.5f,0.0f)),Quaternion.identity);
ph.transform.LookAt(Camera.main.transform.position);
ph.transform.position += (ph.transform.forward * 2.0f);
ph.gameObject.SetActive(true);
}
}
}
return;
}
}
Hit脚本主要Code如下:
using UnityEngine;
using System.Collections;
public class Hit : MonoBehaviour
{
public TextMesh yellow;
public TextMesh black;
float startTime;
float currentTime;
public float lifespan; //in seconds
public float fadeTime; //in seconds
public float fadeSpeed; //in seconds
public float maxSize;
public float speed; // meters per second
public string text;
// Use this for initialization
void Start ()
{
startTime = Time.fixedTime;
}
// Update is called once per frame
void Update ()
{
//destroy this hit report if we are past it's lifespan
currentTime = Time.fixedTime - startTime;
if(currentTime > lifespan)
{
Destroy(gameObject);
}
//fade out
if(currentTime > fadeTime)
{
float fade = (fadeTime -(currentTime-fadeTime)) * (1/fadeSpeed);
Color tempColor = yellow.color;
tempColor.a = fade;
yellow.color = tempColor;
tempColor = black.color;
tempColor.a = fade;
black.color = tempColor;
}
//scale and lift accordingly
if(transform.localScale.x < maxSize)
{
transform.localScale = new Vector3 (1.0f,1.0f,1.0f) * (0.01f + currentTime) * 3.0f;
}
transform.position += (Vector3.up * Time.deltaTime * speed);
transform.LookAt(Camera.main.transform.position);
yellow.text = text;
black.text = text;
}
}
NavMesh
自动寻路则主要是用的Unity3D的NavMesh系统实现:
* 搭建好具体场景
* 选择地面与可以产生碰撞的物体
* 将上述物体设为Navigation Static
* 点击Bake进行路线烘焙
* 编写脚本实现鼠标点击自动寻路
* 将脚本绑定到要自动寻路的角色
脚本Code如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class navmesh : MonoBehaviour {
private NavMeshAgent mr;
// Use this for initialization
void Start () {
mr = GetComponent<NavMeshAgent> ();
}
// Update is called once per frame
void Update () {
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit rayhit;
if (Physics.Raycast(ray, out rayhit))
{
if (rayhit.transform.name == "Terrain")
{
mr.SetDestination(rayhit.point);
}
}
}
}
}