I'm making a 2D game that the player can't touch the light source, so I typed the light code :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Shadow2DLight : MonoBehaviour
{
public float range = 5.0f;
public float intensity = 1.0f;
public Color color = Color.white;
//Internal fields, cache V and P matrix for this frame.
internal Matrix4x4[] V = new Matrix4x4[4]; //Four matricies for right, down, left ,up
internal Matrix4x4 P;
public static List<Shadow2DLight> lights = new List<Shadow2DLight>();
// Start is called before the first frame update
private void OnEnable() {
lights.Add(this);
}
private void OnDisable() {
lights.Remove(this);
}
}
then the block code :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
[RequireComponent(typeof(SpriteRenderer))]
public class Shadow2DCaster : MonoBehaviour
{
private new SpriteRenderer renderer;
private Mesh shadowMesh;
private void Awake() {
renderer = GetComponent<SpriteRenderer>();
shadowMesh = new Mesh();
shadowMesh.vertices = renderer.sprite.vertices.Select(t => new Vector3(t.x, t.y, 0.0f)).ToArray();
var originalTriangles = renderer.sprite.triangles;
var newTriangleIndicies = new List<int>();
for (int i = 0; i < originalTriangles.Length / 3; i++) {
newTriangleIndicies.Add(originalTriangles[3 * i]);
newTriangleIndicies.Add(originalTriangles[3 * i + 1]);
newTriangleIndicies.Add(originalTriangles[3 * i + 1]);
newTriangleIndicies.Add(originalTriangles[3 * i + 2]);
newTriangleIndicies.Add(originalTriangles[3 * i + 2]);
newTriangleIndicies.Add(originalTriangles[3 * i]);
}
shadowMesh.SetIndices(newTriangleIndicies.ToArray(), MeshTopology.Lines, 0);
shadowMesh.UploadMeshData(true);
}
public Mesh GetLineMesh() {
return shadowMesh;
}
public static List<Shadow2DCaster> casters = new List<Shadow2DCaster>();
private static Dictionary<Sprite, Mesh> shadowMeshCache;
private void OnEnable() {
casters.Add(this);
}
private void OnDisable() {
casters.Remove(this);
}
}
then the Shadow2DPipeline code :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[RequireComponent(typeof(Camera))]
public class Shadow2DPipeline : MonoBehaviour {
private Material shadowMapMaterial;
private Material lightingMaterial;
private RenderTexture shadowMap;
private CommandBuffer shadowMapCmdBuffer;
private CommandBuffer lightingCmdBuffer;
private MaterialPropertyBlock propertyBlock;
private Mesh lightingQuad;
private const int MAX_LIGHTS_COUNT = 64;
private const int SHADOW_MAP_WIDTH = 256;
private int ShadowMapHeight {
get {
return MAX_LIGHTS_COUNT * 4;
}
}
private void Start() {
propertyBlock = new MaterialPropertyBlock();
shadowMap = new RenderTexture(SHADOW_MAP_WIDTH, ShadowMapHeight, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear);
shadowMap.filterMode = FilterMode.Point;
shadowMap.antiAliasing = 1;
shadowMap.anisoLevel = 0;
shadowMapMaterial = new Material(Resources.Load<Shader>("2DShadowMap"));
lightingMaterial = new Material(Resources.Load<Shader>("2DAdditiveLight"));
{
lightingQuad = new Mesh();
lightingQuad.vertices = new Vector3[] {
new Vector3(-1.0f, -1.0f),
new Vector3(-1.0f, 1.0f),
new Vector3(1.0f, -1.0f),
new Vector3(1.0f, 1.0f),
};
lightingQuad.triangles = new int[] {
0, 1, 2, 2, 1, 3
};
lightingQuad.UploadMeshData(true);
}
}
private void OnPreRender() {
var cam = GetComponent<Camera>();
var shadowCasters = Shadow2DCaster.casters;
var lights = Shadow2DLight.lights;
{ //Update shadow map rendering command buffer.
if (shadowMapCmdBuffer == null) {
shadowMapCmdBuffer = new CommandBuffer();
cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, shadowMapCmdBuffer);
}
shadowMapCmdBuffer.Clear();
/*
* Shadow caster mesh is line-strip mesh.
*
* In shadow map, each 4 rows belong to one light, corresponding to depth in (0, 90), (90, 180)...(270, 360).
*
* To render shadow mesh at correct position, a naive MVP matrix is used.
* Then, after transforming to clip space, manually edit y component to the corresponding row.
*/
shadowMapCmdBuffer.SetRenderTarget(shadowMap);
shadowMapCmdBuffer.ClearRenderTarget(true, true, Color.black);
shadowMapCmdBuffer.SetGlobalVector(ShaderKeys.ShadowMap2DSize, new Vector4(1.0f / shadowMap.width, 1.0f / shadowMap.height, shadowMap.width, shadowMap.height));
//Calculate V and P for each light.
foreach (var light in lights) {
for (int i = 0; i < 4; i++) { //four directions.
//This TRS here calculates light->world matrix.
//The Quaternion.Euler(90.0f * i, 90.0f, 270.0f) aligns light to right, down, left, up.
//-1.0f in z-component in scale corrects camera forward direction.
var V = Matrix4x4.TRS(light.transform.position, Quaternion.Euler(90.0f * i, 90.0f, 270.0f), new Vector3(1.0f, 1.0f, -1.0f));
//Inverse it, so it's a world->light matrix.
V = V.inverse;
var P = Matrix4x4.Perspective(90.0f, 1.0f, 0.01f, 10.0f);
P = GL.GetGPUProjectionMatrix(P, true);
light.V[i] = V;
light.P = P;
}
}
foreach (var shadowCaster in shadowCasters) {
var M = shadowCaster.transform.localToWorldMatrix;
for (int iLight = 0; iLight < Mathf.Min(MAX_LIGHTS_COUNT, lights.Count); iLight++) {
//Render shadow map of each light, by drawing shadow line mesh onto the shadowmap.
for (int iDir = 0; iDir < 4; iDir++) { //Four directions, right, down, left, up.
var light = lights[iLight];
var MVP = light.P * light.V[iDir] * M;
shadowMapCmdBuffer.SetGlobalMatrix(ShaderKeys.ShadowMap2DMVP, MVP);
float writeClipSpaceY = (iLight * 4 + iDir + 0.5f) / (float)(ShadowMapHeight);
shadowMapCmdBuffer.SetGlobalFloat(ShaderKeys.ShadowMap2DWriteRow, (writeClipSpaceY - 0.5f) * 2.0f);
shadowMapCmdBuffer.DrawMesh(shadowCaster.GetLineMesh(), Matrix4x4.identity /*Use our own MVP matrix*/, shadowMapMaterial);
}
}
}
}
{ //Update lighting command buffer.
//Draw a "light quad" for each light.
//
//There's lots of ways to add light to 2d scene.
//Here we just use an additive "layer"
//Edit to what you like.
if (lightingCmdBuffer == null) {
lightingCmdBuffer = new CommandBuffer();
lightingCmdBuffer.name = "2D Lighting";
cam.AddCommandBuffer(CameraEvent.AfterForwardAlpha, lightingCmdBuffer);
}
lightingCmdBuffer.Clear();
lightingCmdBuffer.SetGlobalTexture(ShaderKeys.ShadowMap2D, shadowMap);
for (int iLight = 0; iLight < Mathf.Min(MAX_LIGHTS_COUNT, lights.Count); iLight++) {
var light = lights[iLight];
lightingCmdBuffer.SetGlobalVector(ShaderKeys.LightParams, new Vector4(light.range, light.intensity, light.transform.position.x, light.transform.position.y));
lightingCmdBuffer.SetGlobalVector(ShaderKeys.LightColor, light.color);
lightingCmdBuffer.SetGlobalFloat(ShaderKeys.ShadowMap2DLightIndex, (float)iLight);
for (int iVP = 0; iVP < ShaderKeys._ShadowMap2DVP.Length; iVP++) {
lightingCmdBuffer.SetGlobalMatrix(ShaderKeys._ShadowMap2DVP[iVP], light.P * light.V[iVP]);
}
var M = Matrix4x4.TRS(light.transform.position, Quaternion.identity, Vector3.one * light.range);
lightingCmdBuffer.SetGlobalMatrix(ShaderKeys.LightM, M);
lightingCmdBuffer.DrawMesh(lightingQuad, M, lightingMaterial);
}
}
}
public static class ShaderKeys {
public static int ShadowMap2DSize = Shader.PropertyToID("_ShadowMap2DSize");
public static int ShadowMap2DMVP = Shader.PropertyToID("_ShadowMap2DMVP");
public static int ShadowMap2DWriteRow = Shader.PropertyToID("_ShadowMap2DWriteRow");
public static int ShadowMap2D = Shader.PropertyToID("_ShadowMap2D");
public static int ShadowMap2DLightIndex = Shader.PropertyToID("_ShadowMap2DLightIndex");
public static int[] _ShadowMap2DVP = new int[] {
Shader.PropertyToID("_ShadowMap2DVP_Right"),
Shader.PropertyToID("_ShadowMap2DVP_Down"),
Shader.PropertyToID("_ShadowMap2DVP_Left"),
Shader.PropertyToID("_ShadowMap2DVP_Up"),
};
public static int LightM = Shader.PropertyToID("_LightM");
public static int LightParams = Shader.PropertyToID("_LightParams");
public static int LightColor = Shader.PropertyToID("_LightColor");
}
}
but how i can detect if the player touch the light??
you can get the source code from here : https://github.com/yangrc1234/Unity2DShadowMap
preview from here : https://i.stack.imgur.com/oGgr2.gif
So one solution would be not to directly detect any shadows, but leverage the essential properties of shadows to get the same effect.
An area that is in shadow is in shadow because there is an opaque object between it and the light source.
An area that is in light is in light because there are no objects between it and the light source.
Therefore you can simply detect if there is an object between your player and the light source using a raycast and use that information to determine if you are in shadow/light.
Very roughly:
Vector3 playerToLight = light.transform.position - player.transform.position;
float distance = playerToLight.magnitude;
vctor3 origin = player.transform.position;
vector3 direction = playerToLight.normalized;
if(Physics.Raycast(origin,direction,distance)){
// in shadow
} else {
// in light
}
Related
I need Help, I'm doing a smaller puzzle game but I have trouble getting the playing field to the center.
Playing field is still on the bottom/left
I tried to fix it using the forum but nothing happened.
Thanks for your help.
Problem
GameBoard.cs
using UnityEngine;
using System.Collections;
public class GameBoard : MonoBehaviour {
public int m_size;
public GameObject m_puzzlePiece;
public GameObject buttonNext;
private PuzzleSection[,] m_puzzle;
private PuzzleSection m_puzzleSelection;
public int m_randomPasses = 12;
void Start()
{
GameObject temp;
m_puzzle = new PuzzleSection[m_size, m_size];
for (int i=0; i< m_size; i++)
{
for (int j=0; j< m_size; j++)
{
temp = (GameObject)Instantiate(m_puzzlePiece,
new Vector2(i*700/m_size, j*700/m_size), Quaternion.identity);
temp.transform.SetParent(transform);
m_puzzle[i, j] = (PuzzleSection)temp.GetComponent<PuzzleSection>();
m_puzzle[i, j].CreatePuzzlePiece(m_size);
}
}
SetupBoard();
RandomizePlacement();
}
void RandomizePlacement()
{
VectorInt2[] puzzleLocation = new VectorInt2[2];
Vector2[] puzzleOffset = new Vector2[2];
do
{
for (int i = 0; i < m_randomPasses; i++)
{
puzzleLocation[0].x = Random.Range(0, m_size);
puzzleLocation[0].y = Random.Range(0, m_size);
puzzleLocation[1].x = Random.Range(0, m_size);
puzzleLocation[1].y = Random.Range(0, m_size);
puzzleOffset[0] = m_puzzle[puzzleLocation[0].x, puzzleLocation[0].y].GetImageOffset();
puzzleOffset[1] = m_puzzle[puzzleLocation[1].x, puzzleLocation[1].y].GetImageOffset();
m_puzzle[puzzleLocation[0].x, puzzleLocation[0].y].AssignImage(puzzleOffset[1]);
m_puzzle[puzzleLocation[1].x, puzzleLocation[1].y].AssignImage(puzzleOffset[0]);
}
} while (CheckBoard() == true);
}
void SetupBoard()
{
Vector2 offset;
Vector2 m_scale = new Vector2(1f / m_size, 1f / m_size);
for (int i=0; i< m_size; i++)
{
for (int j=0; j< m_size; j++)
{
offset = new Vector2(i * (1f / m_size), j * (1f / m_size));
m_puzzle[i, j].AssignImage(m_scale, offset);
}
}
}
public PuzzleSection GetSelection()
{
return m_puzzleSelection;
}
public void SetSelection(PuzzleSection selection)
{
m_puzzleSelection = selection;
}
public bool CheckBoard()
{
for (int i=0; i<m_size; i++)
{
for(int j=0; j< m_size; j++)
{
if (m_puzzle[i, j].CheckGoodPlacement() == false)
return false;
}
}
return true;
}
public void Win()
{
buttonNext.SetActive(true);
}
}
PuzzleSection.cs
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class PuzzleSection : MonoBehaviour {
Vector2 m_goodOffset;
Vector2 m_offset;
Vector2 m_scale;
GameBoard m_gameBoard;
void Start()
{
m_gameBoard = (GameBoard)GameObject.FindObjectOfType<Canvas>()
.GetComponentInChildren<GameBoard>();
}
public void CreatePuzzlePiece(int size)
{
transform.localScale = new Vector3(1.0f * transform.localScale.x / size,
1.0f * transform.localScale.z / size, 1);
}
public void AssignImage(Vector2 scale, Vector2 offset)
{
m_goodOffset = offset;
m_scale = scale;
AssignImage(offset);
}
public void AssignImage(Vector2 offset)
{
m_offset = offset;
GetComponent<RawImage>().uvRect = new Rect(offset.x, offset.y, m_scale.x, m_scale.y);
}
public void OnClick()
{
PuzzleSection previousSelection = m_gameBoard.GetSelection();
if (previousSelection != null)
{
previousSelection.GetComponent<RawImage>().color = Color.white;
Vector2 tempOffset = previousSelection.GetImageOffset();
previousSelection.AssignImage(m_offset);
AssignImage(tempOffset);
m_gameBoard.SetSelection(null);
if (m_gameBoard.CheckBoard() == true)
m_gameBoard.Win();
} else
{
GetComponent<RawImage>().color = Color.gray;
m_gameBoard.SetSelection(this);
}
}
public Vector2 GetImageOffset()
{
return m_offset;
}
public bool CheckGoodPlacement()
{
return (m_goodOffset == m_offset);
}
}
Hey guys, I need Help, I'm doing a smaller puzzle game but I have trouble getting the playing field to the center.
Playing field is still on the bottom/left
I tried to fix it using the forum but nothing happened.
Thanks for your help.
As i gather from the picture your gameboard gets drawn inside a Panel ui control.
You could set the position of this panel to the centre, so that all its children will be centred alongside it:
MyPanel.transform.position = new Vector3 (Screen.width * 0.5f, Screen.height * 0.5f, 0);
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveCameraBehind : MonoBehaviour
{
public GameObject camera;
public List<GameObject> targets = new List<GameObject>();
public float cameraDistance = 10.0f;
public bool behindMultipleTargets = false;
public string cameraWarningMsgs = "";
public string targetsWarningMsgs = "";
// Use this for initialization
void Start()
{
if (camera == null)
{
var cam = GetComponent<Camera>();
if (cam != null)
{
cameraWarningMsgs = "Gettig camera component.";
camera = transform.gameObject;
}
else
{
cameraWarningMsgs = "Creating a new camera component.";
GameObject NewCam = Instantiate(new GameObject(), transform);
NewCam.name = "New Camera";
NewCam.AddComponent<Camera>();
camera = NewCam;
}
}
if(targets.Count == 0)
{
targetsWarningMsgs = "No targets found.";
}
}
void FixedUpdate()
{
if (targets.Count > 0)
{
MoveCameraToPosition();
}
}
public void MoveCameraToPosition()
{
if (targets.Count > 1 && behindMultipleTargets == true)
{
var center = CalculateCenter();
transform.position = new Vector3(center.x, center.y + 2, center.z + cameraDistance);
}
if (behindMultipleTargets == false)
{
Vector3 center = targets[0].transform.position - targets[0].transform.forward * cameraDistance;
transform.position = new Vector3(center.x, center.y + 2, center.z);
}
}
private Vector3 CalculateCenter()
{
Vector3 center = new Vector3();
var totalX = 0f;
var totalY = 0f;
foreach (var target in targets)
{
totalX += target.transform.position.x;
totalY += target.transform.position.y;
}
var centerX = totalX / targets.Count;
var centerY = totalY / targets.Count;
center = new Vector3(centerX, centerY);
return center;
}
}
The CalculateCenter function make the targets(objects) to change positions and vanish away far away. Even if there is only one single target.
What I want to do is if there is one object for example one 3d cube position the camera behind the cube. And if there are more cubes for example two or ten and the camera is somewhere else calculate the middle position behind the targets and position the camera in the middle behind them.
To show what I mean in this example the view(like a camera) is behind the two soldiers in the middle position between them from behind.
But what if there are 5 soldiers how can I find the middle position and then position the camera behind them like this example in the screenshot ?
This is my old script version was working fine but only for 1 or 2 targets. But if there are 5 targets(soldiers) how can I position the camera behind them in the middle ? Like in the screenshot example.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveCameraBehind : MonoBehaviour
{
public GameObject camera;
public List<GameObject> targets = new List<GameObject>();
public float cameraDistance = 10.0f;
public bool behindTwoTargets = false;
public string warningMsgs = "";
// Use this for initialization
void Start()
{
if (camera == null)
{
var cam = GetComponent<Camera>();
if (cam != null)
{
warningMsgs = "Gettig Camera omponent.";
camera = transform.gameObject;
}
else
{
warningMsgs = "Creating a new camera component.";
GameObject NewCam = Instantiate(new GameObject(), transform);
NewCam.name = "New Camera";
NewCam.AddComponent<Camera>();
camera = NewCam;
}
}
camera.transform.Rotate(0, 180, 0);
}
void FixedUpdate()
{
if (targets.Count > 0)
{
MoveCameraToPosition();
}
}
public void MoveCameraToPosition()
{
if (targets.Count == 2 && behindTwoTargets == true)
{
Vector3 center = ((targets[0].transform.position - targets[1].transform.position) / 2.0f) + targets[1].transform.position;
camera.transform.position = new Vector3(center.x, center.y + 2, center.z + cameraDistance);
}
if (behindTwoTargets == false)
{
Vector3 center = targets[0].transform.position - targets[0].transform.forward * cameraDistance;
camera.transform.position = new Vector3(center.x, center.y + 2, center.z);
}
}
}
This is my last version of my working code still using the CalculateCenter function :
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class CameraLook : MonoBehaviour
{
public GameObject camera;
public List<GameObject> targets = new List<GameObject>();
public float cameraDistance = 10.0f;
public float cameraHeight = 2f;
public float rotateTime = 2f;
public bool multipleTargets = false;
public bool changeRandomTarget = false;
public bool behindFront = false;
public bool targetsRandomRot = false;
public string cameraWarningMsgs = "";
public string targetsWarningMsgs = "";
private List<Vector3> vectors = new List<Vector3>();
//Random move rotation timer part
Quaternion qTo;
float speed = 3f;
float timer = 0.0f;
// Use this for initialization
void Start()
{
qTo = Quaternion.Euler(new Vector3(0.0f, Random.Range(-180.0f, 180.0f), 0.0f));
if (camera == null)
{
var cam = GetComponent<Camera>();
if (cam != null)
{
cameraWarningMsgs = "Gettig camera component.";
camera = transform.gameObject;
}
else
{
cameraWarningMsgs = "Creating a new camera component.";
GameObject NewCam = Instantiate(new GameObject(), transform);
NewCam.name = "New Camera";
NewCam.AddComponent<Camera>();
camera = NewCam;
}
}
if (targets.Count == 0)
{
targetsWarningMsgs = "No targets found.";
}
else
{
foreach(GameObject vector in targets)
{
vectors.Add(vector.transform.position);
}
}
}
void FixedUpdate()
{
if (targets.Count > 0)
{
MoveCameraToPosition();
if (targetsRandomRot == true)
{
RotateTargetsRandom();
}
}
}
public void MoveCameraToPosition()
{
Vector3 center = CalculateCenter();
camera.transform.position = center;
if (behindFront == false)
{
camera.transform.rotation = Quaternion.LookRotation(-center, Vector3.up);
}
else
{
camera.transform.rotation = Quaternion.LookRotation(center, Vector3.up);
}
}
private Vector3 CalculateCenter()
{
Vector3 center = new Vector3();
var x = targets[0].transform.position.x;
var y = targets[0].transform.position.y;
var z = targets[0].transform.position.z;
if (multipleTargets == true)
{
for (int i = 1; i < targets.Count; i++)
{
x += targets[i].transform.position.x;
y += targets[i].transform.position.y;
z += targets[i].transform.position.z;
}
}
else
{
x += targets[0].transform.position.x;
y += targets[0].transform.position.y;
z += targets[0].transform.position.z;
}
if(changeRandomTarget == true)
{
for (int i = 1; i < targets.Count; i++)
{
x += targets[i].transform.position.x;
y += targets[i].transform.position.y;
z += targets[i].transform.position.z;
}
}
x = x / targets.Count;
y = y / targets.Count;
z = z / targets.Count;
if (behindFront == false)
{
center = new Vector3(x, y + cameraHeight, z + cameraDistance);
}
else
{
center = new Vector3(x, y + cameraHeight, z - cameraDistance);
}
return center;
}
private void RotateTargetsRandom()
{
timer += Time.deltaTime;
if (timer > rotateTime)
{ // timer resets at 2, allowing .5 s to do the rotating
qTo = Quaternion.Euler(new Vector3(0.0f, Random.Range(-180.0f, 180.0f), 0.0f));
timer = 0.0f;
}
foreach (var target in targets)
{
target.transform.rotation = Quaternion.Slerp(target.transform.rotation, qTo, Time.deltaTime * speed);
}
}
}
And now I want to add and use the bool flag changeRandomTarget :
But not sure how to do it :
if(changeRandomTarget == true)
{
for (int i = 1; i < targets.Count; i++)
{
x += targets[i].transform.position.x;
y += targets[i].transform.position.y;
z += targets[i].transform.position.z;
}
}
I want that each X seconds it will pick a random center and change the camera position according to it. For example the first item in the targets List the last item and all the items so each X seconds the center will be once behind targets[0] or targets1 or targets[i]
Not sure how to do it with my code or with derHugo solution.
Surely you just find the average
so
if (mobcount > 1)
{
var x=mob[0].position.x;
var y=mob[0].position.y;
var z=mob[0].position.z;
for(int i=1; i<mobcount; i++)
{
x += mob[i].position.x;
y += mob[i].position.y;
z += mob[i].position.z;
}
x = x / mobcount;
y = y / mobcount;
z = z / mobcount;
}
therefore the camera should look at the position x,y,z.. and perhaps set the distance to be a fixed distance behind the nearest mob...
Actually you don't even need to do it component wise:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
...
private static Vector3 Average(IReadOnlyCollection<Vector3> vectors)
{
if(vectors == null) return Vector3.zero;
switch (vectors.Count)
{
case 0:
return Vector3.zero;
case 1:
return vectors.First();
default:
var average = Vector3.zero;
foreach(var vector in vectors)
{
average += vector;
}
return average / vectors.Count;
}
}
Or directly using Linq aggregate
private static Vector3 Average(IReadOnlyCollection<Vector3> vectors)
{
if(vectors == null) return Vector3.zero;
switch (vectors.Count)
{
case 0:
return Vector3.zero;
case 1:
return vectors.First();
default:
var average = vectors.Aggregate(Vector3.zero, (current, vector) => current + vector);
return average / vectors.Count;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class SquadFormation : MonoBehaviour
{
enum Formation
{
Square, Circle, Triangle
}
[Header("Main Settings")]
[Space(5)]
public Transform squadMemeberPrefab;
[Range(4, 100)]
public int numberOfSquadMembers = 20;
[Range(1, 20)]
public int numberOfSquads = 1;
[Range(0, 4)]
public int columns = 4;
public int gaps = 10;
public int circleRadius = 10;
public float yOffset = 0;
[Range(3, 50)]
public float moveSpeed = 3;
[Range(3, 50)]
public float rotateSpeed = 1;
public float threshold = 0.1f;
public bool randomSpeed = false;
[Range(1, 100)]
public int randSpeedMin = 1;
[Range(1, 100)]
public int randSpeedMax = 1;
public bool startRandomFormation = false;
public string currentFormation;
private Formation formation;
private List<Quaternion> quaternions = new List<Quaternion>();
private List<Vector3> newpositions = new List<Vector3>();
private bool move = false;
private bool squareFormation = false;
private List<GameObject> squadMembers = new List<GameObject>();
private float[] step;
private int[] randomSpeeds;
private int index = 0;
private int numofobjects = 0;
// Use this for initialization
void Start()
{
numofobjects = numberOfSquadMembers;
if (startRandomFormation)
{
formation = (Formation)UnityEngine.Random.Range(0, Enum.GetNames(typeof(Formation)).Length);
}
else
{
formation = Formation.Square;
}
currentFormation = formation.ToString();
ChangeFormation();
foreach (Transform child in gameObject.transform)
{
if (child.tag == "Squad Member")
squadMembers.Add(child.gameObject);
}
randomSpeeds = RandomNumbers(randSpeedMin, randSpeedMax, squadMembers.Count);
step = new float[squadMembers.Count];
}
// Update is called once per frame
void Update()
{
if (numofobjects != numberOfSquadMembers)
{
numofobjects = 0;
numofobjects = numberOfSquadMembers;
squadMembers = new List<GameObject>();
FormationSquare();
}
if (Input.GetKeyDown(KeyCode.F))
{
randomSpeeds = RandomNumbers(randSpeedMin, randSpeedMax, squadMembers.Count);
foreach (int speedV in randomSpeeds)
{
if (index == randomSpeeds.Length)
index = 0;
step[index] = speedV * Time.deltaTime;
index++;
}
ChangeFormation();
}
if (move == true)
{
MoveToNextFormation();
}
}
private void ChangeFormation()
{
switch (formation)
{
case Formation.Square:
FormationSquare();
break;
case Formation.Circle:
FormationCircle();
break;
}
}
private Vector3 FormationSquarePositionCalculation(int index) // call this func for all your objects
{
float posX = (index % columns) * gaps;
float posY = (index / columns) * gaps;
return new Vector3(posX, posY);
}
private void FormationSquare()
{
newpositions = new List<Vector3>();
quaternions = new List<Quaternion>();
Transform go = squadMemeberPrefab;
for (int i = 0; i < numofobjects; i++)
{
if (squadMembers.Count == 0)
go = Instantiate(squadMemeberPrefab);
Vector3 pos = FormationSquarePositionCalculation(i);
go.position = new Vector3(transform.position.x + pos.x, 0, transform.position.y + pos.y);
go.Rotate(new Vector3(0, -90, 0));
go.tag = "Squad Member";
go.transform.parent = gameObject.transform;
newpositions.Add(go.transform.position);
}
move = true;
squareFormation = true;
formation = Formation.Circle;
}
private Vector3 FormationCirclePositionCalculation(Vector3 center, float radius, int index, float angleIncrement)
{
float ang = index * angleIncrement;
Vector3 pos;
pos.x = center.x + radius * Mathf.Sin(ang * Mathf.Deg2Rad);
pos.z = center.z + radius * Mathf.Cos(ang * Mathf.Deg2Rad);
pos.y = center.y;
return pos;
}
private void FormationCircle()
{
newpositions = new List<Vector3>();
quaternions = new List<Quaternion>();
Vector3 center = transform.position;
float radius = (float)circleRadius / 2;
float angleIncrement = 360 / (float)numberOfSquadMembers;
for (int i = 0; i < numberOfSquadMembers; i++)
{
Vector3 pos = FormationCirclePositionCalculation(center, radius, i, angleIncrement);
var rot = Quaternion.LookRotation(center - pos);
pos.y = Terrain.activeTerrain.SampleHeight(pos);
pos.y = pos.y + yOffset;
newpositions.Add(pos);
quaternions.Add(rot);
}
move = true;
squareFormation = false;
formation = Formation.Square;
}
private void MoveToNextFormation()
{
if (randomSpeed == false)
{
if (step.Length > 0)
step[0] = moveSpeed * Time.deltaTime;
}
for (int i = 0; i < squadMembers.Count; i++)
{
squadMembers[i].transform.LookAt(newpositions[i]);
if (randomSpeed == true)
{
squadMembers[i].transform.position =
Vector3.MoveTowards(squadMembers[i].transform.position, newpositions[i], step[i]);
}
else
{
squadMembers[i].transform.position =
Vector3.MoveTowards(squadMembers[i].transform.position, newpositions[i], step[0]);
}
if (Vector3.Distance(squadMembers[i].transform.position, newpositions[i]) < threshold)
{
if (squareFormation == true)
{
Vector3 degrees = new Vector3(0, 0, 0);
Quaternion quaternion = Quaternion.Euler(degrees);
squadMembers[i].transform.rotation = Quaternion.Slerp(squadMembers[i].transform.rotation, quaternion, rotateSpeed * Time.deltaTime);
}
else
{
squadMembers[i].transform.rotation = Quaternion.Slerp(squadMembers[i].transform.rotation, quaternions[i], rotateSpeed * Time.deltaTime);
}
}
}
}
private static int[] RandomNumbers(int min, int max, int howMany)
{
int[] myNumbers = new int[howMany];
for (int i = 0; i < howMany; i++)
{
myNumbers[i] = UnityEngine.Random.Range(min, max);
}
return myNumbers;
}
}
In the constructor I'm searching for childs with the tag Squad Member.
But the List squadMembers will be empty since the script is attached to a new empty GameObject without any childs.
Then also the variable step will be empty.
Then inside the method MoveToNextFormation I'm checking if step is empty or not:
if (step.Length > 0)
step[0] = moveSpeed * Time.deltaTime;
If not checking the it will throw exception since there is nothing at index 0 it's null. But then if step is empty there will be no speed/s at all for the objects movements.
That's one problem.
I'm not sure even why in the constructor I did the part with the children and the "Squad Member" tag. I'm not creating yet any children with this tag so I'm confused about what I tried to do in the constructor.
The second problem is in this lines in the FormationSquare method:
if (squadMembers.Count == 0)
go = Instantiate(squadMemeberPrefab);
But if squadMembers is empty then it will throw exception somewhere else in other places in the code. And I'm creating new objects inside the FormationSquare method that's since I'm starting by default with the FormationSquare but what if I want to start by default with the FormationCircle method ?
The idea is to start with minimum (1) number of squads and with minimum (4) number of members in the squad when starting the program. Or to start with any range between min and max. But it's all messed up.
In your case, I would separate the squad member prefab instantiation from the squad shape formatting, doing this will help you identify your bug.
For example add the following methods and use them during 'Start':
void AddSquadMember()
{
// Use this to instantiate/spawn a new game object prefab.
}
void AddSquadMember(GameObject object)
{
// Use this for game object already in the scene. (.eg the children with your tag)
}
Then on the formation methods remove the intantiate calls and just use whatever game object you have in the list.
Finally, I would toss the 'numofobjects' variable. Then use 'squadMembers.Count' instead of both 'numofobjects' and 'numberOfSquadMembers' assuming that during 'Start' you have taken care of instantiating all game objects in order to 'numberOfSquadMembers == squadMembers.Count'. That is because you might need to raise the squad with a few more members during gameplay.
In the manager:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FormationsManager : MonoBehaviour
{
public Transform squadMemeberPrefab;
public int numberOfSquadMembers = 20;
public int columns = 4;
public int gaps = 10;
public Formations formations;
private int numofmembers;
// Use this for initialization
void Start()
{
numofmembers = numberOfSquadMembers;
formations.Init(numberOfSquadMembers, columns, gaps);
GenerateSquad();
}
// Update is called once per frame
void Update()
{
if (numofmembers != numberOfSquadMembers)
{
GenerateSquad();
}
}
private void GenerateSquad()
{
Transform go = squadMemeberPrefab;
for (int i = 0; i < formations.newpositions.Count; i++)
{
go = Instantiate(squadMemeberPrefab);
go.position = formations.newpositions[i];
go.tag = "Squad Member";
go.transform.parent = gameObject.transform;
}
}
}
And the Formations script:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Formations : MonoBehaviour
{
public List<Vector3> newpositions;
private int numberOfSquadMembers;
private int columns;
private int gaps;
private List<Quaternion> quaternions;
private Vector3 FormationSquarePositionCalculation(int index)
{
float posX = (index % columns) * gaps;
float posY = (index / columns) * gaps;
return new Vector3(posX, posY);
}
private void FormationSquare()
{
newpositions = new List<Vector3>();
quaternions = new List<Quaternion>();
for (int i = 0; i < numberOfSquadMembers; i++)
{
Vector3 pos = FormationSquarePositionCalculation(i);
Vector3 position = new Vector3(transform.position.x + pos.x, 0, transform.position.y + pos.y);
newpositions.Add(position);
}
}
public void Init(int numberOfSquadMembers, int columns, int gaps)
{
this.numberOfSquadMembers = numberOfSquadMembers;
this.columns = columns;
this.gaps = gaps;
FormationSquare();
}
}
What I want to do is in the FormationsManager in the Update not only just calling GenerateSquad but to add the new once to the last/next position of the existing already formation.
void Update()
{
if (numofmembers != numberOfSquadMembers)
{
GenerateSquad();
}
}
If the value of numberOfSquadMembers is 20 first time and then I changed it to 21 add new object to the end of the formation and same if I change the value of numberOfSquadMembers for example from 20 to 19 or from 21 to 5 destroy the amount of objects from the end and keep the formation shape.
The soldiers the last line is on the right side.
So if I change the value to add more then add it to the right and if I change to less destroy from the right side. The most left line of soldiers is the first.
It is possible if you keep GameObject instances inside FormationsManager class, and then reuse them in GenerateSquad method.
In FormationsManager class, add and modify code as follows.
public GameObject squadMemeberPrefab;
List<GameObject> SquadMembers = new List<GameObject>();
void Update()
{
if (numofmembers != numberOfSquadMembers)
{
numofmembers = numberOfSquadMembers;
formations.Init(numberOfSquadMembers, columns, gaps);
GenerateSquad();
}
}
private void GenerateSquad()
{
Transform go = squadMemeberPrefab;
List<GameObject> newSquadMembers = new List<GameObject>();
int i = 0;
for (; i < formations.newpositions.Count; i++)
{
if (i < SquadMembers.Count)
go = SquadMembers[i];
else
{
go = Instantiate(squadMemeberPrefab);
newSquadMembers.Add(go);
}
go.position = formations.newpositions[i];
go.tag = "Squad Member";
go.transform.parent = gameObject.transform;
}
for (; i < SquadMembers.Count; i++)
Destroy(SquadMembers[i]);
SquadMembers = newSquadMembers;
}
However, I recommend you to consider GameObject Pool (Object Pool), which can thoroughly resolve such object recycle problem. For this purpose, you can use ClientScene.RegisterSpawnHandler. Go to this Unity Documentation page and search text "GameObject pool". You can see an example code there.
I have this pooling script:
using UnityEngine;
using System.Collections.Generic;
public class ObjectPool
{
private GameObject prefab;
private List<GameObject> pool;
public ObjectPool(GameObject prefab, int initialSize)
{
this.prefab = prefab;
this.pool = new List<GameObject>();
for (int i = 0; i < initialSize; i++)
{
AllocateInstance();
}
}
public GameObject GetInstance()
{
if (pool.Count == 0)
{
AllocateInstance();
}
int lastIndex = pool.Count - 1;
GameObject instance = pool[lastIndex];
pool.RemoveAt(lastIndex);
instance.SetActive(true);
return instance;
}
public void ReturnInstance(GameObject instance)
{
instance.SetActive(false);
pool.Add(instance);
}
protected virtual GameObject AllocateInstance()
{
GameObject instance = (GameObject)GameObject.Instantiate(prefab);
instance.SetActive(false);
pool.Add(instance);
return instance;
}
}
And i'm using it with this script to instantiate objects.
It should put the objects in random positions around the terrain area.
But instead all the objects are in the same position at x = 0 , y = 20 , z = 0
Not random at all.
using System;
using UnityEngine;
using Random = UnityEngine.Random;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class InstantiateObjects : MonoBehaviour
{
public GameObject Spaceship;
public int spaceshipsStartingHeight = 20;
[HideInInspector]
public GameObject[] spaceships;
// for tracking properties change
private Vector3 _extents;
private int _spaceshipCount;
private float _spaceshipSize;
private List<int> randomNumbers = new List<int>();
private ObjectPool bulletPool;
/// <summary>
/// How far to place spheres randomly.
/// </summary>
public Vector3 Extents;
/// <summary>
/// How many spheres wanted.
/// </summary>
public int SpaceShipCount;
public float SpaceShipSize;
// Use this for initialization
void Start()
{
rndNumbers();
Clone();
spaceships = GameObject.FindGameObjectsWithTag("SpaceShip");
}
private void OnValidate()
{
// prevent wrong values to be entered
Extents = new Vector3(Mathf.Max(0.0f, Extents.x), Mathf.Max(0.0f, Extents.y), Mathf.Max(0.0f, Extents.z));
SpaceShipCount = Mathf.Max(0, SpaceShipCount);
SpaceShipSize = Mathf.Max(0.0f, SpaceShipSize);
}
private void Reset()
{
Extents = new Vector3(250.0f, 20.0f, 250.0f);
SpaceShipCount = 100;
SpaceShipSize = 20.0f;
}
// Update is called once per frame
void Update()
{
}
private void Clone()
{
if (Extents == _extents && SpaceShipCount == _spaceshipCount && Mathf.Approximately(SpaceShipSize, _spaceshipSize))
return;
// cleanup
var ShipsToDestroy = GameObject.FindGameObjectsWithTag("SpaceShip");
foreach (var t in ShipsToDestroy)
{
if (Application.isEditor)
{
DestroyImmediate(t);
}
else
{
Destroy(t);
}
}
var withTag = GameObject.FindWithTag("Terrain");
if (withTag == null)
throw new InvalidOperationException("Terrain not found");
bulletPool = new ObjectPool(Spaceship, SpaceShipCount);
for (var i = 0; i < SpaceShipCount; i++)
{
GameObject o = bulletPool.GetInstance();
o.tag = "SpaceShip";
o.transform.SetParent(base.gameObject.transform);
o.transform.localScale = new Vector3(SpaceShipSize, SpaceShipSize, SpaceShipSize);
// get random position
var x = Random.Range(-Extents.x, Extents.x);
var y = Extents.y; // sphere altitude relative to terrain below
var z = Random.Range(-Extents.z, Extents.z);
// now send a ray down terrain to adjust Y according terrain below
var height = 10000.0f; // should be higher than highest terrain altitude
var origin = new Vector3(x, height, z);
var ray = new Ray(origin, Vector3.down);
RaycastHit hit;
var maxDistance = 20000.0f;
var nameToLayer = LayerMask.NameToLayer("Terrain");
var layerMask = 1 << nameToLayer;
if (Physics.Raycast(ray, out hit, maxDistance, layerMask))
{
var distance = hit.distance;
y = height - distance + y; // adjust
}
else
{
Debug.LogWarning("Terrain not hit, using default height !");
}
//o.transform.Rotate(0.0f,randomNumbers[i],0.0f);
// place !
o.transform.position = new Vector3(x, y + spaceshipsStartingHeight, z);
}
_extents = Extents;
_spaceshipCount = SpaceShipCount;
_spaceshipSize = SpaceShipSize;
}
public void rndNumbers()
{
}
}
The answer is to remove the whole // cleanUp part inside the Clone(). Now it's creating random objects using the pooling.