Boids – 2D Unity Approach

Boid’s model was created in 1986 by Craig Reynolds to simulate the behaviour of birds as they group together in a flock. The model has three key methods to mimic real life bird flock behaviour. These methods include:

a) Maintaining a heading based on the averaged direction of surrounding boids.
b) Maintaining a separation so boids do not overlap.
c) The boids must maintain a cohesiveness with the flock by steering towards the centre position of surrounding flockmates.

The behaviour also operates similar to schools of fish or other wildlife. As a game developer, this can be useful when implementing enemy mob behaviour and in my case, I used this approach to simulate flocks of zombies in 2d space as seen in the mp4 above.

The concept isn’t too difficult, below I’ve posted two implementations. I’ve implemented it using rigidbody2d but I’ve also included the code for using it without rigidbody force.

  1. Boid Class for use with Rigidbody2D
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
public class BoidRigidbody2D : MonoBehaviour
{
//our boid's rigidbody
public Rigidbody2D thisRigidbody2D;
//predefined array to prevent unity GC, predefinied to keep GC low.
private Collider2D[] surroundingColliders = new Collider2D[10];
private int surroundingColliderNonAllocLength = 0;
//filtered array of surrounding units, predefinied to keep GC low.
private BoidRigidbody2D[] surroundingUnits = new BoidRigidbody2D[10];
private int surroundingUnitsLength = 0;
//adjustable control variables
public float scanRadius = 1f;
public float scanDistance = 0.5f;
public float avoidRadius = 0.15f;
public float avoidObjectsScanDistance = 1.0f;
public float cohesionAmount = 0.5f;
public float moveSpeed = 3f;
public float maxSpeed = 5f;
public float currentAvoidForce = 0;
public float avoidForceProximityMultiplier = 100;
public float avoidObjectDirectionFallOffSpeed = 20f;
public float avoidObjectMultiplier = 20f;
public float avergaeDirectionMultiplier = 5f;
public float avoidDirectionMultiplier = 1f;
//controls to enable or disable aspects of Boids algorithm
public bool useAverageDirection;
public bool useAvoidUnitsDirection;
public bool useAvoidObjects;
public bool useCohesion;
//other variables for tracking an
private Vector2 lastPos = Vector2.zero;
private Vector2 avoidObjectDirection = Vector2.zero;
//Unity inherited methods:
/// <summary>
/// Unity Start Method - Called at beginning of object's life, good time to initialize if required.
/// </summary>
private void Start()
{
//random start velocity to get some movement going.
Vector2 randomVelocity = new Vector2(Mathf.PerlinNoise(Random.Range(-1000f, 1000f), Random.Range(-1000f, 1000f)) * Random.Range(-1f, 1f),
Mathf.PerlinNoise(Random.Range(-1000f, 1000f), Random.Range(-1000f, 1000f)) * Random.Range(-1f, 1f)).normalized;
thisRigidbody2D.velocity = randomVelocity.normalized;
}
/// <summary>
/// Unity FixedUpdate Method - called in-step with unity physics engine.
/// </summary>
private void FixedUpdate()
{
Move();
}
//BoidsRigidbody2D methods:
/// <summary>
/// Scan for surrounding flockmates using a NonAllocOverlapCircle. NonAlloc used to reduce memory usage and GC collection.
/// </summary>
/// <param name="scanRadius"></param>
void GetSurroundingUnits(float scanRadius)
{
surroundingColliderNonAllocLength = Physics2D.OverlapCircleNonAlloc(this.transform.position, scanRadius, surroundingColliders, 1 << LayerMask.NameToLayer("NPCS"));
surroundingUnitsLength = 0;
for (int i = 0; i < surroundingColliderNonAllocLength; i++)
{
//prevent including self
if (surroundingColliders[i].gameObject != this.gameObject)
{
surroundingUnits[surroundingUnitsLength] = surroundingColliders[i].GetComponent<BoidRigidbody2D>();
surroundingUnitsLength++;
}
}
}/// <summary>
/// Gets the average direction of local flockmates.
/// </summary>
/// <returns></returns>
private Vector2 GetAverageDirection(float scanRadius)
{
if (!useAverageDirection)
return Vector2.zero;
Vector2 averageDirection = Vector2.zero;
//use for loop instead of foreach, for loops are more optimized in Unity for memory management at high frame rate.
for (int i = 0; i < surroundingUnitsLength; i++)
{
//get distance between this unit and surrounding unit
float distance = Vector2.Distance(this.transform.position, surroundingUnits[i].transform.position);
Vector2 velocity = surroundingUnits[i].currentDirection;//.thisRigidbody2D.velocity.normalized;
velocity = velocity / distance;
//add unit's direction to overall average.
averageDirection += velocity;
}
return averageDirection;
}
/// <summary>
/// Gets direction opposite to avoid crowding and overlap of flockmates.
/// </summary>
/// <returns></returns>
private Vector2 GetAvoidDirection(float avoidRadius)
{
if (!useAvoidUnitsDirection)
return Vector2.zero;
Vector2 oppositeDirection = Vector2.zero;
//use for loop instead of foreach, for loops are more optimized in Unity for memory management at high frame rate.
for (int i = 0; i < surroundingUnitsLength; i++)
{
float distance = Vector2.Distance(this.transform.position, surroundingUnits[i].transform.position);
if (distance <= avoidRadius)
{
Vector2 diff = this.transform.position - surroundingUnits[i].transform.position;
diff.Normalize();
diff = diff / distance;
oppositeDirection += diff;
}
}
return oppositeDirection;
}
public Vector2 currentDirection
{
get { return thisRigidbody2D.velocity.normalized; }
}
/// <summary>
/// Gets direction opposite to avoid bumping into map objects (i.e. walls)
/// </summary>
/// <returns></returns>
Vector2 ScanForObjectsToAvoid()
{
Vector2 avgDirection = Vector2.zero;
Vector3 longestOpenPathDirection = Vector3.zero;
Vector3 longestClosedPathDirection = Vector3.zero;
bool hitSomething = false;
currentAvoidForce = 1;
float lastHitDistance = 0;
//get angle of current heading to do some trig
float angle = Mathf.Atan2(currentDirection.y, currentDirection.x) * Mathf.Rad2Deg;
//we're doing 3 raycasts
for (int t = 0; t < 3; t++)
{
RaycastHit2D raycastHit2DResult; // Gizmos.DrawLine(this.transform.position + new Vector3(0, -avoidRadius / 2f, 0), this.transform.position + (new Vector3(0, -avoidRadius / 2f, 0) + new Vector3(facingDirection.x, facingDirection.y, 0) * avoidRadius));
Vector2 currentRayDir = Vector2.zero;
if (t == 0)
{
//raycast forward
currentRayDir = currentDirection * (avoidObjectsScanDistance + 0.5f);
raycastHit2DResult = Physics2D.Raycast(this.transform.position, currentRayDir, avoidObjectsScanDistance + 0.5f, 1 << LayerMask.NameToLayer("Map"));
Debug.DrawRay(this.transform.position, currentDirection * (avoidObjectsScanDistance + 0.5f), Color.blue, 0.1f);
}
else if (t == 1)
{
//upward relative angle to direction of movement to be used for raycast.
Vector2 dir = DegreeToVector2(75 + angle);
dir = (currentDirection + dir).normalized * avoidObjectsScanDistance;
currentRayDir = dir;
raycastHit2DResult = Physics2D.Raycast(this.transform.position, dir, avoidObjectsScanDistance, 1 << LayerMask.NameToLayer("Map"));
Debug.DrawRay(this.transform.position, dir, Color.red, 0.1f);
}
else
{
//downward relative angle to direction of movement to be used for raycast.
Vector2 dir = DegreeToVector2(-75 + angle);
dir = (currentDirection + dir).normalized * avoidObjectsScanDistance;
currentRayDir = dir;
raycastHit2DResult = Physics2D.Raycast(this.transform.position, dir, avoidObjectsScanDistance, 1 << LayerMask.NameToLayer("Map"));
Debug.DrawRay(this.transform.position, dir, Color.green, 0.1f);
}
//check if we hit anything and if mutliple raycasts hit something, we want to find the one furthest away.
if (raycastHit2DResult.collider != null)
{
hitSomething = true;
float dist = Vector2.Distance(this.transform.position.xy(), raycastHit2DResult.point);
//if this point was further than other hit points, we might potentially head in this direction
if (dist > lastHitDistance)
{
lastHitDistance = dist;
longestClosedPathDirection = currentRayDir;
}
}
//if the raycast didn't hit anything then that is currently longest open path
else
{
longestOpenPathDirection = currentRayDir;
}
}
//if no raycasts hit anything, we should return nothing and keep heading in current direction.
if (!hitSomething)
{
return Vector2.zero;
}
//if all three directional raycasts hit something, we'll head towards direction with furthest distance between self and hit point.
else if (longestOpenPathDirection == Vector3.zero)
{
return longestClosedPathDirection;
}
//if one of the raycasts were open but others hit something, we'll head in direction of ray that didn't hit anything.
else
{
return longestOpenPathDirection;
}
}
/// <summary>
/// Gets direction towards average position of local flockmates.
/// Can be used to keep boids closer or further apart.
/// </summary>
/// <returns></returns>
private Vector2 GetCohesionDirection(float cohesion)
{
if (!useCohesion)
return Vector2.zero;
Vector2 directionToCentre = Vector2.zero;
Vector2 centrePosition = Vector2.zero;
for (int i = 0; i < surroundingUnitsLength; i++)
{
float distance = Vector2.Distance(this.transform.position, surroundingUnits[i].transform.position);
if (distance <= scanRadius)
{
centrePosition += surroundingUnits[i].transform.position.xy();
}
}
if (surroundingUnitsLength > 0)
{
centrePosition = centrePosition / surroundingUnitsLength;
directionToCentre = centrePosition - this.transform.position.xy();
Debug.DrawRay(this.transform.position, directionToCentre.normalized * 2,Color.blue, 0.2f);
}
return directionToCentre * cohesion;
}
/// <summary>
/// Move this boid by applying 2d physical force.
/// </summary>
private void Move()
{
//calculate direction we want to move to using boids
Vector2 moveDirection = BoidsMoveDirection();
moveDirection *= moveSpeed;
thisRigidbody2D.AddForce(moveDirection);
//prevent going over max speed
if (thisRigidbody2D.velocity.magnitude > maxSpeed)
thisRigidbody2D.velocity = Vector2.ClampMagnitude(thisRigidbody2D.velocity, maxSpeed);
}
/// <summary>
/// Calculate the direction to move taking into account other surrounding flockmates and objects
/// </summary>
/// <returns></returns>
private Vector2 BoidsMoveDirection()
{
Vector2 moveDirection = Vector2.zero;
//check if any surrounding units.
GetSurroundingUnits(scanRadius);
//get direction this unit should move based on surrounding units.
moveDirection += GetAverageDirection(scanDistance) * avergaeDirectionMultiplier;
moveDirection += GetAvoidDirection(avoidRadius) * avoidDirectionMultiplier;
//scan for objects to avoid and then smoothly adjust to prevent sudden hit in direction change.
var avoidObjectDirectionT = ScanForObjectsToAvoid().normalized * avoidObjectMultiplier;
if (avoidObjectDirectionT.magnitude > 0)
avoidObjectDirection = avoidObjectDirectionT;
avoidObjectDirection = Vector2.Lerp(avoidObjectDirection, Vector2.zero, Time.deltaTime * avoidObjectDirectionFallOffSpeed);
if (avoidObjectDirection.magnitude < 0.01f)
{
avoidObjectDirection = Vector2.zero;
}
moveDirection += avoidObjectDirection;
moveDirection += GetCohesionDirection(cohesionAmount);
return moveDirection.normalized;
}
/// <summary>
/// Convert radian to Vector2
/// </summary>
/// <param name="radian"></param>
/// <returns></returns>
public Vector2 RadianToVector2(float radian)
{
return new Vector2(Mathf.Cos(radian), Mathf.Sin(radian));
}
/// <summary>
/// Convert degree to Vector2
/// </summary>
/// <param name="degree"></param>
/// <returns></returns>
public Vector2 DegreeToVector2(float degree)
{
return RadianToVector2(degree * Mathf.Deg2Rad);
}
}
view raw BoidRigidbody2D.cs hosted with ❤ by GitHub
  1. Boid Class for use with kinematic
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boids : MonoBehaviour {
//predefined array to keep unity GC low
private Collider2D[] surroundingCollidersNonAlloc = new Collider2D[10];
private int surroundingColliderNonAllocLength = 0;
//filtered array of surrounding units, predefinied to keep GC low.
private BoidsDemo[] surroundingUnits = new BoidsDemo[10];
private int surroundingUnitsLength = 0;
//adjustable control variables
public float scanRadius = 1f;
public float scanDistance = 0.5f;
public float avoidRadius = 0.15f;
public float avoidObjectsScanDistance = 1.0f;
public float cohesionAmount = 0.5f;
public float moveSpeed = 3f;
public float maxSpeed = 5f;
public float avoidObjectDirectionFallOffSpeed = 20f;
public float avoidObjectMultiplier = 20f;
public float avergaeDirectionMultiplier = 5f;
public float avoidDirectionMultiplier = 1f;
//controls to enable or disable aspects of Boids algorithm
public bool useAverageDirection;
public bool useAvoidUnitsDirection;
public bool useAvoidObjects;
public bool useCohesion;
//other variables
private Vector2 lastPos = Vector2.zero;
private Vector2 randomStartDirection;
private Vector2 avoidObjectDirection = Vector2.zero;
private void Start()
{
Random.InitState(Mathf.RoundToInt(Time.frameCount));
currentHeading = new Vector2(Random.Range(-1, 1), Random.Range(-1, 1));
}
void Update ()
{
Move();
CalcHeading();
}
/// <summary>
/// Scan for surrounding flockmates using a NonAllocOverlapCircle. NonAlloc used to reduce memory usage and GC collection.
/// </summary>
/// <param name="scanRadius"></param>
void GetSurroundingUnits(float scanRadius)
{
surroundingColliderNonAllocLength = Physics2D.OverlapCircleNonAlloc(this.transform.position, scanRadius, surroundingCollidersNonAlloc, 1 << LayerMask.NameToLayer("NPCS"));
surroundingUnitsLength = 0;
for (int i = 0; i < surroundingColliderNonAllocLength; i++)
{
//prevent including self
if (surroundingCollidersNonAlloc[i].gameObject != this.gameObject)
{
surroundingUnits[surroundingUnitsLength] = surroundingCollidersNonAlloc[i].GetComponent<BoidsDemo>();
surroundingUnitsLength++;
}
}
}
/// <summary>
/// Gets the average direction of local flockmates.
/// </summary>
/// <returns></returns>
private Vector2 GetAverageDirection(float scanRadius)
{
if (!useAverageDirection)
return Vector2.zero;
Vector2 averageDirection = Vector2.zero;
//use for loop instead of foreach, for loops are more optimized in Unity for memory management at high frame rate.
for (int i = 0; i < surroundingUnitsLength; i++)
{
//get distance between this unit and surrounding unit
float distance = Vector2.Distance(this.transform.position, surroundingUnits[i].transform.position);
Vector2 velocity = surroundingUnits[i].currentHeading.normalized;//.thisRigidbody2D.velocity.normalized;
velocity = velocity / distance;
//add unit's direction to overall average.
averageDirection += velocity;
}
if (surroundingUnitsLength > 0)
return averageDirection / surroundingUnitsLength;
else return averageDirection;
}
/// <summary>
/// Gets direction opposite to avoid crowding and overlap of flockmates.
/// </summary>
/// <param name="avoidRadius">Radius to scan for clockmates to avoid</param>
/// <returns></returns>
private Vector2 GetAvoidDirection(float avoidRadius)
{
if (!useAvoidUnitsDirection)
return Vector2.zero;
Vector2 avoidDirection = Vector2.zero;
//use for loop instead of foreach, for loops are more optimized in Unity for memory management at high frame rate.
for (int i = 0; i < surroundingUnitsLength; i++)
{
float distance = Vector2.Distance(this.transform.position, surroundingUnits[i].transform.position);
if (distance <= avoidRadius)
{
Vector2 diff = this.transform.position - surroundingUnits[i].transform.position;
diff.Normalize();
diff = diff / distance;//weight based on distance.
avoidDirection += diff;
}
}
return avoidDirection;
}
public Vector2 currentHeading;
/// <summary>
/// Calculate the current direction we're heading.
/// </summary>
void CalcHeading()
{
//good old trig math to calculate current direction based on last position.
Vector2 dir = (this.transform.position.xy() - lastPos) / Time.deltaTime;
currentHeading = dir.normalized;
lastPos = this.transform.position.xy();
}
public Vector2 scanAngle = new Vector2(75, -75);
public float currentAvoidForce = 0;
public float avoidForceProximityMultiplier = 100;
/// <summary>
/// Gets direction opposite to avoid bumping into map objects (i.e. walls)
/// </summary>
/// <returns></returns>
Vector2 ScanForObjectsToAvoid()
{
Vector2 avgDirection = Vector2.zero;
Vector3 longestOpenPathDirection = Vector3.zero;
Vector3 longestClosedPathDirection = Vector3.zero;
bool hitSomething = false;
currentAvoidForce = 1;
float lastHitDistance = 0;
//get angle of current heading
float angle = Mathf.Atan2(currentHeading.y, currentHeading.x) * Mathf.Rad2Deg;
//upward relative angle to direction of movement.
Vector2 dirOne = DegreeToVector2(scanAngle.x + angle);
dirOne = (currentHeading + dirOne).normalized * avoidObjectsScanDistance;
//downward relative angle to direction of movement.
Vector2 dirTwo = DegreeToVector2(scanAngle.y + angle);
dirTwo = (currentHeading + dirTwo).normalized * avoidObjectsScanDistance;
//we're doing 3 raycasts
for (int t = 0; t < 3; t++)
{
RaycastHit2D raycastHit2DResult;
Vector2 currentRayDir = Vector2.zero;
if (t == 0)
{
currentRayDir = currentHeading * (avoidObjectsScanDistance + 0.5f);
raycastHit2DResult = Physics2D.Raycast(this.transform.position, currentRayDir, avoidObjectsScanDistance + 0.5f, 1 << LayerMask.NameToLayer("Map"));
Debug.DrawRay(this.transform.position, currentHeading * (avoidObjectsScanDistance + 0.5f), Color.blue, 0.1f);
}
else if (t == 1)
{
currentRayDir = dirOne;
raycastHit2DResult = Physics2D.Raycast(this.transform.position, dirOne, avoidObjectsScanDistance, 1 << LayerMask.NameToLayer("Map"));
Debug.DrawRay(this.transform.position, dirOne, Color.red, 0.1f);
}
else {
currentRayDir = dirTwo;
raycastHit2DResult = Physics2D.Raycast(this.transform.position, dirTwo, avoidObjectsScanDistance, 1 << LayerMask.NameToLayer("Map"));
Debug.DrawRay(this.transform.position, dirTwo, Color.green, 0.1f);
}
//check if we hit anything
if (raycastHit2DResult.collider != null)
{
hitSomething = true;
float dist = Vector2.Distance(this.transform.position.xy(), raycastHit2DResult.point);
if (dist > lastHitDistance)
{
lastHitDistance = dist;
longestClosedPathDirection = currentRayDir;
}
}
//if the raycast didn't hit anything then that is currently longest open path
else
{
longestOpenPathDirection = currentRayDir;
}
}
//if no raycasts hit anything, we should return nothing and keep heading in current direction.
if(!hitSomething)
{
return Vector2.zero;
}
else if(longestOpenPathDirection == Vector3.zero)
{
return longestClosedPathDirection;
}
else// if(longestOpenPathDirection == Vector3.zero)
{
return longestOpenPathDirection;
}
// if (longestOpenPathDirection.xy() != Vector2.zero)
// DrawArrow.ForDebug(this.transform.position, longestOpenPathDirection* currentAvoidForce * 0.5f, Color.cyan, 0.5f);
// return longestOpenPathDirection;
}
/// <summary>
/// Gets direction towards average position of local flockmates.
/// Can be used to keep boids closer or further apart.
/// </summary>
/// <returns></returns>
private Vector2 GetCohesionDirection(float cohesion)
{
if (!useCohesion)
return Vector2.zero;
Vector2 directionToCentre = Vector2.zero;
Vector2 centrePosition = Vector2.zero;
for (int i = 0; i < surroundingUnitsLength; i++)
{
float distance = Vector2.Distance(this.transform.position, surroundingUnits[i].transform.position);
if (distance <= scanRadius)
{
centrePosition += surroundingUnits[i].transform.position.xy();
}
}
if (surroundingUnitsLength > 0)
{
//Debug.DrawRay(this.transform.position, centrePosition.normalized * 2, Color.cyan, 0.2f);
centrePosition = centrePosition / surroundingUnitsLength;
directionToCentre = centrePosition - this.transform.position.xy();
// Debug.DrawRay(this.transform.position, directionToCentre.normalized * 2,Color.blue, 0.2f);
}
return directionToCentre * cohesion;
}
/// <summary>
/// Move this boid transform to target position based on MoveDirection calculation
/// </summary>
private void Move()
{
//move our boid transform.
Vector3 moveDirection = (MoveDirection()+currentHeading).normalized;
if (moveDirection.magnitude == 0)
moveDirection = currentHeading;
this.transform.position = Vector2.MoveTowards(this.transform.position, this.transform.position + moveDirection, moveSpeed * Time.deltaTime);
}
/// <summary>
/// Calculate the direction to move taking into account other surrounding flockmates and objects
/// </summary>
/// <returns></returns>
private Vector2 MoveDirection()
{
Vector2 moveDirection = Vector2.zero;
//check if any surrounding units.
GetSurroundingUnits(scanRadius);
//get direction this unit should move based on surrounding units.
moveDirection += GetAverageDirection(scanDistance) * avergaeDirectionMultiplier;
moveDirection += GetAvoidDirection(avoidRadius) * avoidDirectionMultiplier;
//scan for objects to avoid and then smoothly adjust to prevent sudden hit in direction change.
var avoidObjectDirectionT = ScanForObjectsToAvoid().normalized * avoidObjectMultiplier;
if (avoidObjectDirectionT.magnitude > 0)
avoidObjectDirection = avoidObjectDirectionT;
avoidObjectDirection = Vector2.Lerp(avoidObjectDirection, Vector2.zero, Time.deltaTime * avoidObjectDirectionFallOffSpeed);
if(avoidObjectDirection.magnitude < 0.01f)
{
avoidObjectDirection = Vector2.zero;
}
moveDirection += avoidObjectDirection;
moveDirection += GetCohesionDirection(cohesionAmount);
return moveDirection.normalized;
}
/// <summary>
/// Convert radian to Vector2
/// </summary>
/// <param name="radian"></param>
/// <returns></returns>
public Vector2 RadianToVector2(float radian)
{
return new Vector2(Mathf.Cos(radian), Mathf.Sin(radian));
}
/// <summary>
/// Convert degree to Vector2
/// </summary>
/// <param name="degree"></param>
/// <returns></returns>
public Vector2 DegreeToVector2(float degree)
{
return RadianToVector2(degree * Mathf.Deg2Rad);
}
}
view raw Boids.cs hosted with ❤ by GitHub

Simple Unity Android VR controller setup for Xbox 360 Controllers

It is no longer required to download the Android VR SDK to build a mobile VR game or application for Unity. Instead, you can set your game up in the Unity build settings and make use of UnityEngine.VR.

Creating a simple player controller may be a bit tricky and so I hope this post will save some developers a head ache when it comes to moving a player around with a gamepad in Android based VR projects.

In this setup, I’m using the following:
1 x Android 6.0 Phone
1 x XBOX 360 Wired USB Controller (but you can use any Android VR controller)
1 x Generic Android VR Headset

To begin, it’s important to understand that you have to create a parent and child arrangement of GameObject’s to control a VR player. Whereas generally a simple FPS controller might be arranged with a Camera as a child to a Capsule, with Unity VR you’ll have to child the Camera to a Camera parent Transform and that Camera parent will be a child to your main player. Even more, the Body of the player will have to be a separate child Transform of the parenting Main Player GameObject if your player has a body. Why? This is done to control the movement of the Camera since moving the Camera Transform directly in VR seems to result in odd behaviour/non-functioning movement at the time of this post.

Here is how your basic Main Player hierarchy should look:

Step 2:
Attach a camera to the Camera GameObject.

Step 3:
In your input settings, add two new axes. For the two different Android phones I tested, the Xbox 360 controller I had seemed to map to these axis:

Step 4:

Create a new script and call it SimpleVRController.cs Use the code below then assign the proper transforms in the inspector.

/*************************************************************************
* SimpleVRPlayerController created by Chris @ ChrisHammond.ca
* __________________
*
* Usage: Please feel free to use this code for your own personal or commercial
* projects if it helps or is any benefit to you.
*
*
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.VR;
public class SimpleVRPlayerController : Photon.MonoBehaviour
{
public enum InputType { KeyboardAndMouse, OneVRHandheldController }
//set to keyboard and mouse in inspector if you want to test in editor without controller
public InputType inputType = InputType.OneVRHandheldController;
//movement and character controller variables
public float moveSpeed = 5f;
public float lookSpeed = 1f;
public float jumpSpeed = 8.0F;
public float gravity = 20.0F;
public CharacterController characterController;
//camera
public Transform vrCameraTransform;
//camera parent - used to move camera/change position of cam
public Transform vrCameraTransformParent;
//player body parent for player movement
public Transform playerBodyTransform;
//variable for capsule controller direction
private Vector3 moveDirection = Vector3.zero;
//audio for footsteps
public AudioSource footSteps;
//mouse control variables for testing on PC
private Vector3 rotation;
public float mouseSensitivity = 100.0f;
public float clampAngle = 80.0f;
void Start()
{
rotation = vrCameraTransform.rotation.eulerAngles;
}
void Update()
{
//keyboard and mouse for testing purposes on PC
if (inputType == InputType.KeyboardAndMouse)
{
float mouseX = Input.GetAxis("Mouse X");
float mouseY = -Input.GetAxis("Mouse Y");
rotation.y += mouseX * mouseSensitivity * Time.deltaTime;
rotation.x += mouseY * mouseSensitivity * Time.deltaTime;
rotation.x = Mathf.Clamp(rotation.x, -clampAngle, clampAngle);
Quaternion localRotation = Quaternion.Euler(rotation.x, rotation.y, 0.0f);
vrCameraTransform.rotation = localRotation;
float y = moveDirection.y;
moveDirection = (vrCameraTransform.forward * moveSpeed * Input.GetAxis("Vertical")) +
(vrCameraTransform.right * moveSpeed * Input.GetAxis("Horizontal"));
moveDirection.y = y;
vrCameraTransformParent.localPosition = playerBodyTransform.localPosition;
playerBodyTransform.rotation = vrCameraTransform.rotation;
}
else
{
//get forward and right direction of the VR Camera
Vector3 forward = vrCameraTransform.TransformDirection(Vector3.forward).normalized;
Vector3 right = vrCameraTransform.TransformDirection(Vector3.right).normalized;
//y direction for falling/jumping
float y = moveDirection.y;
//use the forward and right direction of Camera for applying force to body in the correct direction.
moveDirection = (forward * moveSpeed * Input.GetAxis("Vertical")) + //up/down on joystic is vertical
(right * moveSpeed * Input.GetAxis("Horizontal")); //left right on joystick is horizontal
moveDirection.y = y;
//set camera parent to match player body position.
//cannot set camera position directly, seems to cause problems/not apply as reported by multiple Unity users.
//so in that case, we we have childed the camera and we move it by moving it's parent.
vrCameraTransformParent.localPosition = playerBodyTransform.localPosition;
playerBodyTransform.rotation = vrCameraTransform.rotation;
}
//if character is on the ground then they can jump if they desire to.
if (characterController.isGrounded)
{
if (Input.GetButton("Jump"))
moveDirection.y = jumpSpeed;
}
//apply gravity to y axis move direction
moveDirection.y -= gravity * Time.deltaTime;
//if player is moving, play footstep sounds.
if (moveDirection.x > 0.1f || moveDirection.x < -0.1f || moveDirection.z < -0.1f || moveDirection.z > 0.1f)
{
if (!footSteps.isPlaying)
{
footSteps.Play();
}
}
else
{
footSteps.Stop();
}
//apply calculated movement to character controller.
characterController.Move(moveDirection * Time.deltaTime);
}
private void OnApplicationPause(bool pauseStatus)
{
InputTracking.Recenter();
}
}

Unity 2D Target Scanner

Target scanners are essential for any game where you have an enemy mob that actively looks for prey. One way to scan for targets is to cast a circular physics ray based on the radius area you would like to scan.

Below is a quick and helpful intro I’ve put together for anyone who needs a quick and easy target scanner to notify another script via event subscription. It searches for targets based on the tags set through the Unity Inspector.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Simple2DTargetScanner : MonoBehaviour
{
public float scanFrequency; //in seconds
public float scanRadius = 5; //in world units
private float scanFrequencyCounter = 0; //counter for time between scans
public string[] targetTagArray; //array of tags of GameObjects to include
//delegate event to notify subscribed scripts
public delegate void TargetHit(Transform target);
public event TargetHit OnTargetHit;
public Color editorCircleColor = Color.green; //circle colour for editor
public bool scannerEnabled = true; //enable or disable scanner (or you can just enable/disable script if you prefer)
// Update is called once per frame
void FixedUpdate()
{
//if not enabled, return to stop code below from running.
if (!scannerEnabled)
return;
//counter to track time between scans
scanFrequencyCounter += Time.deltaTime;
//once counter reaches the total time we set between scans, we scan for target and reset counter to zero.
if (scanFrequencyCounter >= scanFrequency)
{
scanFrequencyCounter = 0;
//using Physics2D we will draw a circle and use a 2d raycast to see what objects are within it.
//Targets must have collider for raycast to return or it won't be detected with this method.
RaycastHit2D[] raycastHit2D = Physics2D.CircleCastAll(this.transform.position, scanRadius, Vector2.right, scanRadius * 2f);
//if we hit any targets, notify listeners that need this info
//make sure to check if OnTargetHit is null or not in case no subscribers (but if no subscribers, why using this at all? 😉 )
if (raycastHit2D.Length > 0 && OnTargetHit != null)
{
for(int i = 0; i < raycastHit2D.Length; i++)
{
//Important note: if you have a lot of target scanners running at once, for performance it might be better to just scan based on
//layers rather than compare gameobject tags.
for (int t = 0; t < targetTagArray.Length; t++)
{
if (raycastHit2D[i].transform.CompareTag(targetTagArray[t]))
{
//delgate event
OnTargetHit(raycastHit2D[i].transform);
//break out of both loops after first hit
i = raycastHit2D.Length + 1;
break;
}
}
}
}
}
}
//Unity editor code. Make sure to use hashtag UNITY_EDITOR so this code doesn't compile to non-editor build.
#if UNITY_EDITOR
//Draw gizmo wire disc (circle) when GameObject is selected.
//If you want it to always draw, then you can use void OnDrawGizmos() instead
void OnDrawGizmosSelected()
{
//set gizmo colour.
UnityEditor.Handles.color = editorCircleColor;
//draw wire circle (disc in unity lingo) based on radius variable you set.
UnityEditor.Handles.DrawWireDisc(this.transform.position, this.transform.forward, scanRadius);
}
#endif
}

How to use from another script:

//Method activated when Simple2DTargetScanner OnTargetHit event is called.
void OnTargetHit(Transform target)
{
//do something with target hit
}
void OnEnable()
{
//subscribe to Simple2DTargetScanner OnTargetHit event notification if GameObject is enabled.
this.transform.GetComponent<Simple2DTargetScanner>().OnTargetHit += OnTargetHit;
}
void OnDisable()
{
//unsubscribe to Simple2DTargetScanner OnTargetHit event notification if GameObject is disabled
this.transform.GetComponent<Simple2DTargetScanner>().OnTargetHit -= OnTargetHit;
}
view raw EnemyMob.cs hosted with ❤ by GitHub

 

Final notes:

This is a simple solution if you only require a few dozen scanners running at a time or less. If you have many more running concurrently then using tags may be slower and I would recommend scanning based on Unity Layers. You can set which layer to RayCast2D on or alternatively, you can set in Project Settings -> Physics 2D and set which physics layers interact with one another.