Related
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class FadeInOut : MonoBehaviour
{
List<Material> mats = new List<Material>();
private void Start()
{
foreach (Transform g in transform.GetComponentsInChildren<Transform>())
{
if (g.GetComponent<SkinnedMeshRenderer>() != null)
{
var l = g.GetComponent<SkinnedMeshRenderer>().sharedMaterials.ToList();
foreach(Material mat in l)
{
mats.Add(mat);
}
}
}
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.G))
{
StartCoroutine(FadeToTogether(mats, 0, 0.5f));
}
}
IEnumerator FadeTo(List<Material> materials, float targetOpacity, float duration)
{
for (int i = 0; i < materials.Count; i++)
{
Color color = materials[i].color;
float startOpacity = color.a;
float t = 0;
while (t < duration)
{
t += Time.deltaTime;
float blend = Mathf.Clamp01(t / duration);
color.a = Mathf.Lerp(startOpacity, targetOpacity, blend);
materials[i].color = color;
yield return null;
}
}
}
IEnumerator FadeToTogether(List<Material> materials, float targetOpacity, float duration)
{
float t = 0;
List<float> startOpacities = new List<float>();
foreach (var material in materials)
{
startOpacities.Add(material.color.a);
}
while (t < duration)
{
t += Time.deltaTime;
float blend = Mathf.Clamp01(t / duration);
for (int i = 0; i < materials.Count; i++)
{
Color color = materials[i].color;
color.a = Mathf.Lerp(startOpacities[i], targetOpacity, blend);
materials[i].color = color;
}
yield return null;
}
yield return new WaitForSeconds(0.1f);
if (targetOpacity == 0)
{
StartCoroutine(FadeToTogether(mats, 1, 0.5f));
}
else
{
StartCoroutine(FadeToTogether(mats, 0, 0.5f));
}
}
}
All the materials rendering mode are set to Fade.
The problem is when i stop the game the alpha color stay in the editor at the value when i stopped the game so the character/object with the materials sometimes is half faded out or sometimes faded out completely.
Is there a way to make that when i stop the game it will set the materials alpha color to 1?
I tried to add this to the bottom of the script but it's not doing anything :
private void OnApplicationQuit()
{
StartCoroutine(FadeToTogether(mats, 1, 0));
}
If you use Time.scale = 0 to stop the game, you should know that yield retun null does not take into account Time.Scale. So change to solve the problem as below:
while (true)
{
Debug.Log("Ignores Time.scale...");
yield return null;
}
while (true)
{
Debug.Log("Considers Time.scale...");
yield return new WaitForSeconds(Time.deltaTime);
}
This is probably happening because when you're changing materials - you don't change something inside GameObjects, you're changing game files.
Application.Quit() doesn't work in editor cos it's a function for compiled game, so if you build it - it will work.
What you can do is copy material's properties into renderer component
GetComponent<Renderer> ().material.CopyPropertiesFromMaterial(mat);
P.S. Didn't test it, so try it and send feedback
P.P.S Also tell me if I didn't understand your question correctly
This is a follow-up question from How to make individual anchor points of bezier continuous or non-continuous. Please refer to it for the relevant code in the accepted answer (please note that I did this to keep this question clean since the related code is quite lengthy).
I am trying to achieve the following:
Make the bezier curve handles/control points selectable in such a way that the properties (for example continuity) for an individual handle are displayed in the inspector window when selected. Please note I'd like this to be done without making creating game objects for the handles/ control points
Retain a single method that handles the movement of each point instead of having separate methods for the movement of each point.
Am I late to the party?
Make the bezier curve handles/control points selectable in such a way that the properties (for example continuity) for an individual handle are displayed in the inspector window when selected. Please note I'd like this to be done without making creating game objects for the handles/ control points
I like #jour's solution in general, except 1 thing: with Handles.Button you have to click a point to select it and then you click and drag to move the control point.
I propose a different approach. Using the same Handles.FreeMoveHandle, but having a variable to remember the id of the last clicked handle, so I can identify it.
Usually, a built in Handle won't give you more info than what it is designed to do. FreeMoveHandle, for example, returns the delta of its translation and that's all. The problem is: You want to capture a simple click, but if you just clicked and didn't drag, the return value is Vector3.zero and it is just the same as if you didn't do a click at all.
Good news: among the overloads of any Handle, there are some calls with an argument named controlID - it is an identifier for each interactable GUI object. If you supress it, the Engine will choose one and you will never know what. But if you pass an int, that value will be the id of the handle. But if I pass an int and it happen to conflict with any of the other ids i don't see? Well, you can call GUIUtility.GetControlID to get a safe id.
Then, it is straightforward. If the id of a Handle is the same as EditorGUIUtility.hotControl (that is, the control that got clicked or have the keyboard focus), then I save the index of this point in selectedControlPointId and use it to display a custom property in Inspector.
Retain a single method that handles the movement of each point instead of having separate methods for the movement of each point.
Hmm... Here become the controversy. If I understood it correctly, you want a single code to draw both nodes and tangents. The thing is: this two things are different in nature. Of course, if you keep it plain and simple, they are bot movable points in scene. But, when you introduces things like constraints (the continuity, or smooth) and selection, they become different beasts, with different logic. Even if you want to make ControlPoint a struct (like i did now) and pass it over as a whole, you'd still need to point what component you are aiming to update, so the constraints will apply to the others - you will always need a "master" field to avoid circular updates (you change tangentBack, and that makes tangentFront to update, that trigger tangentBack to update again and so on).
That's why, even though I reorganized somehow the ControlPoint methods and made it a struct, I can't make a single logic to draw both nodes and tangents.
Here are some codes. I started over from the codes on my answer in the previous question.
ControlPoint.cs
using System;
using UnityEngine;
[Serializable]
public struct ControlPoint
{
public Vector2 position;
public Vector2 tangentBack;
public Vector2 tangentFront;
public bool smooth;
static public ControlPoint MovePosition(ControlPoint pt, Vector2 newPos)
{
var newPt = pt;
newPt.position = newPos;
return newPt;
}
static public ControlPoint MoveTangentBack(ControlPoint pt, Vector2 newTanBack)
{
var newPt = pt;
newPt.tangentBack = newTanBack;
if (pt.smooth) newPt.tangentFront = pt.tangentFront.magnitude * -newTanBack.normalized;
return newPt;
}
static public ControlPoint MoveTangentFront(ControlPoint pt, Vector2 newTanFront)
{
var newPt = pt;
newPt.tangentFront = newTanFront;
if (pt.smooth) newPt.tangentBack = pt.tangentBack.magnitude * -newTanFront.normalized;
return newPt;
}
static public ControlPoint WithSmooth(ControlPoint pt, bool smooth)
{
var newPt = pt;
if (smooth != pt.smooth) newPt.tangentBack = -pt.tangentFront;
return newPt;
}
public ControlPoint(Vector2 position, Vector2 tanBack, Vector2 tanFront, bool smooth = false)
{
this.position = position;
this.tangentBack = tanBack;
this.tangentFront = tanFront;
this.smooth = smooth;
}
}
I removed ControlPointDrawer, so the other propertis you added to it won't be hidden in inspector.
Path.cs
using System;
using UnityEngine;
using System.Collections.Generic;
[Serializable]
public class Path
{
[SerializeField] List<ControlPoint> _points;
[SerializeField] bool _loop = false;
public Path(Vector2 position)
{
_points = new List<ControlPoint>
{
new ControlPoint(position, -Vector2.one, Vector2.one),
new ControlPoint(position + Vector2.right, -Vector2.one, Vector2.one)
};
}
public bool loop { get { return _loop; } set { _loop = value; } }
public ControlPoint this[int i]
{
get { return _points[(_loop && i == _points.Count) ? 0 : i]; }
set { _points[(_loop && i == _points.Count) ? 0 : i] = value; }
}
public int NumPoints { get { return _points.Count; } }
public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }
public ControlPoint InsertPoint(int i, Vector2 position)
{
_points.Insert(i, new ControlPoint(position, -Vector2.one, Vector2.one));
return this[i];
}
public ControlPoint RemovePoint(int i)
{
var item = this[i];
_points.RemoveAt(i);
return item;
}
public Vector2[] GetBezierPointsInSegment(int i)
{
var pointBack = this[i];
var pointFront = this[i + 1];
return new Vector2[4]
{
pointBack.position,
pointBack.position + pointBack.tangentFront,
pointFront.position + pointFront.tangentBack,
pointFront.position
};
}
public ControlPoint MovePoint(int i, Vector2 position)
{
this[i] = ControlPoint.MovePosition(this[i], position);
return this[i];
}
public ControlPoint MoveTangentBack(int i, Vector2 position)
{
this[i] = ControlPoint.MoveTangentBack(this[i], position);
return this[i];
}
public ControlPoint MoveTangentFront(int i, Vector2 position)
{
this[i] = ControlPoint.MoveTangentFront(this[i], position);
return this[i];
}
}
PathCreator.cs is the same
PathCreatorEditor.cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
PathCreator creator;
Path path;
SerializedProperty property;
static int selectedControlPointId = -1;
public override void OnInspectorGUI()
{
serializedObject.Update();
var loopProp = property.FindPropertyRelative("_loop");
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(loopProp);
var ptsProp = property.FindPropertyRelative("_points");
var msg = "Total points in path: " + ptsProp.arraySize + "\n";
if (selectedControlPointId >= 0 && ptsProp.arraySize > 0)
{
EditorGUILayout.HelpBox(msg + "Selected control point: " + selectedControlPointId, MessageType.Info);
EditorGUILayout.Separator();
EditorGUILayout.PropertyField(ptsProp.GetArrayElementAtIndex(selectedControlPointId), true);
}
else
{
EditorGUILayout.HelpBox(msg + "No control points selected", MessageType.Info);
}
if (EditorGUI.EndChangeCheck()) serializedObject.ApplyModifiedProperties();
}
void OnSceneGUI()
{
Input();
Draw();
}
void Input()
{
Event guiEvent = Event.current;
Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
mousePos = creator.transform.InverseTransformPoint(mousePos);
if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
{
Undo.RecordObject(creator, "Insert point");
path.InsertPoint(path.NumPoints, mousePos);
}
else if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.control)
{
for (int i = 0; i < path.NumPoints; i++)
{
if (Vector2.Distance(mousePos, path[i].position) <= .25f)
{
Undo.RecordObject(creator, "Remove point");
path.RemovePoint(i);
break;
}
}
}
}
void Draw()
{
Handles.matrix = creator.transform.localToWorldMatrix;
var rot = Quaternion.Inverse(creator.transform.rotation) * Tools.handleRotation;
var snap = Vector2.zero;
Handles.CapFunction cap = Handles.DotHandleCap;
for (int i = 0; i < path.NumPoints; i++)
{
float size;
var pos = path[i].position;
size = HandleUtility.GetHandleSize(pos) * .05f;
Handles.Label(pos, i.ToString());
Handles.color = i == selectedControlPointId ? Handles.selectedColor : Color.red;
int ctrlId = GUIUtility.GetControlID(FocusType.Passive);
Vector2 newPos = Handles.FreeMoveHandle(ctrlId, pos, rot, size, snap, cap);
if (ctrlId == EditorGUIUtility.hotControl) selectedControlPointId = i;
if (pos != newPos)
{
Undo.RecordObject(creator, "Move point position");
path.MovePoint(i, newPos);
}
pos = newPos;
Handles.color = Color.black;
if (path.loop || i != 0)
{
var tanBack = pos + path[i].tangentBack;
Handles.DrawLine(pos, tanBack);
size = HandleUtility.GetHandleSize(tanBack) * .03f;
Vector2 newTanBack = Handles.FreeMoveHandle(tanBack, rot, size, snap, cap);
if (tanBack != newTanBack)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentBack(i, newTanBack - pos);
}
}
if (path.loop || i != path.NumPoints - 1)
{
var tanFront = pos + path[i].tangentFront;
Handles.DrawLine(pos, tanFront);
size = HandleUtility.GetHandleSize(tanFront) * .03f;
Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
if (tanFront != newTanFront)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentFront(i, newTanFront - pos);
}
}
}
Repaint();
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected | GizmoType.Pickable)]
static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
{
Gizmos.matrix = creator.transform.localToWorldMatrix;
var path = creator.path;
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetBezierPointsInSegment(i);
var pts = Handles.MakeBezierPoints(points[0], points[3], points[1], points[2], 30);
Gizmos.color = Color.green;
for (int j = 0; j < pts.Length - 1; j++)
{
Gizmos.DrawLine(pts[j], pts[j + 1]);
}
}
}
void OnEnable()
{
creator = (PathCreator)target;
path = creator.path ?? creator.CreatePath();
property = serializedObject.FindProperty("path");
}
}
Note: I moved the drawing of the Bezier line from OnSceneGui to DrawGizmo, so the green line will be visible even when the object is not selected, and it will be pickable in the Scene Editor, for having it selected.
Lastly, I would suggest some further development of this scripts. It wouldn't be very hard to make multiple point selection possible. Maybe making the default handles (like position and rotation) to be applicable individually on points. Or changing the method for creating and deleting points to something move intuitive like double-clicking or dragging the path line. Or even a custom toolbar to smart point manipulation, like align, distribute, sculpt... Different constraints, like smooth, symmetric, cusp or straight...
Not entirely sure if I understood the question, but
Make the bezier curve handles/control points selectable in such a way
that the properties (for example continuity) for an individual handle
are displayed in the inspector window when selected. Please note I'd
like this to be done without making creating game objects for the
handles/ control points
You need to use OnSceneGUI option to be able to select the handles, and everytime you select a new point just store its value.
private void OnSceneGUI ()
{
spline = target as Path;
handleTransform = spline.transform;
handleRotation = Tools.pivotRotation == PivotRotation.Local
? handleTransform.rotation : Quaternion.identity;
Vector3 p0 = ShowPoint (0);
Color gg = Color.gray;
gg.a = 0.25f;
for (int i = 1; i < spline.ControlPointCount; i += 3)
{
Vector3 p1 = ShowPoint (i);
Vector3 p2 = ShowPoint (i + 1);
Vector3 p3 = ShowPoint (i + 2);
Handles.color = gg;
Handles.DrawLine (p0, p1);
Handles.DrawLine (p2, p3);
Handles.DrawBezier (p0, p3, p1, p2, Color.white, null, 2f);
p0 = p3;
}
ShowDirections ();
}
private Vector3 ShowPoint (int index)
{
Vector3 point = handleTransform.TransformPoint (spline.Points[index]);
float size = HandleUtility.GetHandleSize (point);
if (index == 0)
{
size *= 2f;
}
Handles.color = modeColors[(int) spline.GetControlPointMode (index)];
if (Handles.Button (point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap))
{
selectedIndex = index;
Repaint ();
}
if (selectedIndex == index)
{
EditorGUI.BeginChangeCheck ();
point = Handles.DoPositionHandle (point, handleRotation);
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Move Point");
EditorUtility.SetDirty (spline);
spline.SetControlPoint (handleTransform.InverseTransformPoint (point), index);
}
}
return point;
}
and for showing the point on inspector GUI
private void DrawSelectedPointInspector ()
{
GUILayout.Label ("Selected Point");
EditorGUI.BeginChangeCheck ();
Vector3 point = EditorGUILayout.Vector3Field ("Position", spline.Points[selectedIndex]);
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Move Point");
EditorUtility.SetDirty (spline);
spline.SetControlPoint (point, selectedIndex);
}
EditorGUI.BeginChangeCheck ();
BezierControlPointModes mode = (BezierControlPointModes) EditorGUILayout.EnumPopup ("Mode", spline.GetControlPointMode (selectedIndex));
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Change Point Mode");
spline.SetControlPointMode (selectedIndex, mode);
EditorUtility.SetDirty (spline);
}
}
and call this on the InspectorGUI
if (selectedIndex >= 0 && selectedIndex < spline.points.Count)
{
DrawSelectedPointInspector ();
}
Retain a single method that handles the movement of each point instead
of having separate methods for the movement of each point.
For this you just need to retain the
I am creating bezier curves with the following code. The curves can be extended to join several bezier curves by shift clicking in the scene view. My code has functionality for making the whole curve continuous or non-continuous. I realised that I need to make individual points (specifically anchor points) have this functionality.
I believe the most ideal way to go about this is creating a new class for the points with this functionality (making points continuous or non-continuous) since this can be used to add other properties that might be specific to the points. How can do this?
Path
[System.Serializable]
public class Path {
[SerializeField, HideInInspector]
List<Vector2> points;
[SerializeField, HideInInspector]
public bool isContinuous;
public Path(Vector2 centre)
{
points = new List<Vector2>
{
centre+Vector2.left,
centre+(Vector2.left+Vector2.up)*.5f,
centre + (Vector2.right+Vector2.down)*.5f,
centre + Vector2.right
};
}
public Vector2 this[int i]
{
get
{
return points[i];
}
}
public int NumPoints
{
get
{
return points.Count;
}
}
public int NumSegments
{
get
{
return (points.Count - 4) / 3 + 1;
}
}
public void AddSegment(Vector2 anchorPos)
{
points.Add(points[points.Count - 1] * 2 - points[points.Count - 2]);
points.Add((points[points.Count - 1] + anchorPos) * .5f);
points.Add(anchorPos);
}
public Vector2[] GetPointsInSegment(int i)
{
return new Vector2[] { points[i * 3], points[i * 3 + 1], points[i * 3 + 2], points[i * 3 + 3] };
}
public void MovePoint(int i, Vector2 pos)
{
if (isContinuous)
{
Vector2 deltaMove = pos - points[i];
points[i] = pos;
if (i % 3 == 0)
{
if (i + 1 < points.Count)
{
points[i + 1] += deltaMove;
}
if (i - 1 >= 0)
{
points[i - 1] += deltaMove;
}
}
else
{
bool nextPointIsAnchor = (i + 1) % 3 == 0;
int correspondingControlIndex = (nextPointIsAnchor) ? i + 2 : i - 2;
int anchorIndex = (nextPointIsAnchor) ? i + 1 : i - 1;
if (correspondingControlIndex >= 0 && correspondingControlIndex < points.Count)
{
float dst = (points[anchorIndex] - points[correspondingControlIndex]).magnitude;
Vector2 dir = (points[anchorIndex] - pos).normalized;
points[correspondingControlIndex] = points[anchorIndex] + dir * dst;
}
}
}
}
else {
points[i] = pos;
}
}
PathCreator
public class PathCreator : MonoBehaviour {
[HideInInspector]
public Path path;
public void CreatePath()
{
path = new Path(transform.position);
}
}
PathEditor
[CustomEditor(typeof(PathCreator))]
public class PathEditor : Editor {
PathCreator creator;
Path path;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUI.BeginChangeCheck();
bool continuousControlPoints = GUILayout.Toggle(path.isContinuous, "Set Continuous Control Points");
if (continuousControlPoints != path.isContinuous)
{
Undo.RecordObject(creator, "Toggle set continuous controls");
path.isContinuous = continuousControlPoints;
}
if (EditorGUI.EndChangeCheck())
{
SceneView.RepaintAll();
}
}
void OnSceneGUI()
{
Input();
Draw();
}
void Input()
{
Event guiEvent = Event.current;
Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
{
Undo.RecordObject(creator, "Add segment");
path.AddSegment(mousePos);
}
}
void Draw()
{
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetPointsInSegment(i);
Handles.color = Color.black;
Handles.DrawLine(points[1], points[0]);
Handles.DrawLine(points[2], points[3]);
Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
}
Handles.color = Color.red;
for (int i = 0; i < path.NumPoints; i++)
{
Vector2 newPos = Handles.FreeMoveHandle(path[i], Quaternion.identity, .1f, Vector2.zero, Handles.CylinderHandleCap);
if (path[i] != newPos)
{
Undo.RecordObject(creator, "Move point");
path.MovePoint(i, newPos);
}
}
}
void OnEnable()
{
creator = (PathCreator)target;
if (creator.path == null)
{
creator.CreatePath();
}
path = creator.path;
}
}
I think your idea is fine: you can write two classes, named ControlPoint and HandlePoint (make them serializable).
ControlPoint may represent p0 and p3 of each curve - the points the path indeed pass through. For continuity, you must assert that p3 of one segment equals to p0 of the next segment.
HandlePoint may represent p1 and p2 of each curve - the points that are tangents of the curve and provide direction and inclination. For smoothness, you must assert that (p3 - p2).normalized of one segment equals to (p1 - p0).normalized of the next segment. (if you want symetric smoothness, p3 - p2 of one must equals p1 - p0 of the other.)
Tip #1: Always consider matrix transformations when assigning or comparing points of each segment. I suggest you to convert any point to global space before performing the operations.
Tip #2: consider applying a constraint between points inside a segment, so when you move arround p0 or p3 of a curve, p1 or p2 move accordingly by the same amount, respectively (just like any graphics editor software do on bezier curves).
Edit -> Code provided
I did a sample implementation of the idea. Actually, after start coding I realized that just one class ControlPoint (instead of two) will do the job. A ControlPoint have 2 tangents. The desired behaviour is controled by the field smooth, that can be set for each point.
ControlPoint.cs
using System;
using UnityEngine;
[Serializable]
public class ControlPoint
{
[SerializeField] Vector2 _position;
[SerializeField] bool _smooth;
[SerializeField] Vector2 _tangentBack;
[SerializeField] Vector2 _tangentFront;
public Vector2 position
{
get { return _position; }
set { _position = value; }
}
public bool smooth
{
get { return _smooth; }
set { if (_smooth = value) _tangentBack = -_tangentFront; }
}
public Vector2 tangentBack
{
get { return _tangentBack; }
set
{
_tangentBack = value;
if (_smooth) _tangentFront = _tangentFront.magnitude * -value.normalized;
}
}
public Vector2 tangentFront
{
get { return _tangentFront; }
set
{
_tangentFront = value;
if (_smooth) _tangentBack = _tangentBack.magnitude * -value.normalized;
}
}
public ControlPoint(Vector2 position, bool smooth = true)
{
this._position = position;
this._smooth = smooth;
this._tangentBack = -Vector2.one;
this._tangentFront = Vector2.one;
}
}
I also coded a custom PropertyDrawer for the ControlPoint class, so it can be shown better on the inspector. It is just a naive implementation. You could improve it very much.
ControlPointDrawer.cs
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(ControlPoint))]
public class ControlPointDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0; //-= 1;
var propPos = new Rect(position.x, position.y, position.x + 18, position.height);
var prop = property.FindPropertyRelative("_smooth");
EditorGUI.PropertyField(propPos, prop, GUIContent.none);
propPos = new Rect(position.x + 20, position.y, position.width - 20, position.height);
prop = property.FindPropertyRelative("_position");
EditorGUI.PropertyField(propPos, prop, GUIContent.none);
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
}
I followed the same architecture of your solution, but with the needed adjustments to fit the ControlPoint class, and other fixes/changes. For example, I stored all the point values in local coordinates, so the transformations on the component or parents reflect in the curve.
Path.cs
using System;
using UnityEngine;
using System.Collections.Generic;
[Serializable]
public class Path
{
[SerializeField] List<ControlPoint> _points;
[SerializeField] bool _loop = false;
public Path(Vector2 position)
{
_points = new List<ControlPoint>
{
new ControlPoint(position),
new ControlPoint(position + Vector2.right)
};
}
public bool loop { get { return _loop; } set { _loop = value; } }
public ControlPoint this[int i] { get { return _points[(_loop && i == _points.Count) ? 0 : i]; } }
public int NumPoints { get { return _points.Count; } }
public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }
public ControlPoint InsertPoint(int i, Vector2 position, bool smooth)
{
_points.Insert(i, new ControlPoint(position, smooth));
return this[i];
}
public ControlPoint RemovePoint(int i)
{
var item = this[i];
_points.RemoveAt(i);
return item;
}
public Vector2[] GetBezierPointsInSegment(int i)
{
var pointBack = this[i];
var pointFront = this[i + 1];
return new Vector2[4]
{
pointBack.position,
pointBack.position + pointBack.tangentFront,
pointFront.position + pointFront.tangentBack,
pointFront.position
};
}
public ControlPoint MovePoint(int i, Vector2 position)
{
this[i].position = position;
return this[i];
}
public ControlPoint MoveTangentBack(int i, Vector2 position)
{
this[i].tangentBack = position;
return this[i];
}
public ControlPoint MoveTangentFront(int i, Vector2 position)
{
this[i].tangentFront = position;
return this[i];
}
}
PathEditor is pretty much the same thing.
PathCreator.cs
using UnityEngine;
public class PathCreator : MonoBehaviour
{
public Path path;
public Path CreatePath()
{
return path = new Path(Vector2.zero);
}
void Reset()
{
CreatePath();
}
}
Finally, all the magic happens in the PathCreatorEditor. Two comments here:
1) I moved the drawing of the lines to a custom DrawGizmo static function, so you can have the lines even when the object is not Active (i.e. shown in the Inspector) You could even make it pickable if you want to. I don't know if you want this behaviour, but you could easily revert;
2) Notice the Handles.matrix = creator.transform.localToWorldMatrix lines over the class. It automatically transforms the scale and rotation of the points to the world coordinates. There is a detail with PivotRotation over there too.
PathCreatorEditor.cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
PathCreator creator;
Path path;
SerializedProperty property;
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(property, true);
if (EditorGUI.EndChangeCheck()) serializedObject.ApplyModifiedProperties();
}
void OnSceneGUI()
{
Input();
Draw();
}
void Input()
{
Event guiEvent = Event.current;
Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
mousePos = creator.transform.InverseTransformPoint(mousePos);
if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
{
Undo.RecordObject(creator, "Insert point");
path.InsertPoint(path.NumPoints, mousePos, false);
}
else if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.control)
{
for (int i = 0; i < path.NumPoints; i++)
{
if (Vector2.Distance(mousePos, path[i].position) <= .25f)
{
Undo.RecordObject(creator, "Remove point");
path.RemovePoint(i);
break;
}
}
}
}
void Draw()
{
Handles.matrix = creator.transform.localToWorldMatrix;
var rot = Tools.pivotRotation == PivotRotation.Local ? creator.transform.rotation : Quaternion.identity;
var snap = Vector2.zero;
Handles.CapFunction cap = Handles.CylinderHandleCap;
for (int i = 0; i < path.NumPoints; i++)
{
var pos = path[i].position;
var size = .1f;
Handles.color = Color.red;
Vector2 newPos = Handles.FreeMoveHandle(pos, rot, size, snap, cap);
if (pos != newPos)
{
Undo.RecordObject(creator, "Move point position");
path.MovePoint(i, newPos);
}
pos = newPos;
if (path.loop || i != 0)
{
var tanBack = pos + path[i].tangentBack;
Handles.color = Color.black;
Handles.DrawLine(pos, tanBack);
Handles.color = Color.red;
Vector2 newTanBack = Handles.FreeMoveHandle(tanBack, rot, size, snap, cap);
if (tanBack != newTanBack)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentBack(i, newTanBack - pos);
}
}
if (path.loop || i != path.NumPoints - 1)
{
var tanFront = pos + path[i].tangentFront;
Handles.color = Color.black;
Handles.DrawLine(pos, tanFront);
Handles.color = Color.red;
Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
if (tanFront != newTanFront)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentFront(i, newTanFront - pos);
}
}
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
{
Handles.matrix = creator.transform.localToWorldMatrix;
var path = creator.path;
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetBezierPointsInSegment(i);
Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
}
}
void OnEnable()
{
creator = (PathCreator)target;
path = creator.path ?? creator.CreatePath();
property = serializedObject.FindProperty("path");
}
}
Moreover, I added a loop field in case you want the curve to be closed, and I added a naive funcionality to remove points by Ctrl+click on the Scene.
Summing up, this is just basic stuff, but you could do it as advanced as you want. Also, you can reuse your ControlPoint class with other Components, like a Catmull-Rom spline, geometric shapes, other parametric functions...
The basic question in your post is: 'Is it a good idea to a have a separate Class for the points of a bezier curve?'
Since the curve will be made up of such points and these are more than just two coordinates imo it most certainly is a good idea.
But, as usual when doing class design, let's collect a few use cases, i.e. things a point will be used for or things we expect to do to a point..:
A point can be added or removed from a curve
A point can be moved
Its control point(s) can be moved
Besides the mere location, a point, i.e. an 'anchor point' should have more properties and abilities/methods..:
It has control points; how these are related to the points is sometimes not exactly the same. Looking at the Unity docs we see that the Handles.DrawLine looks at two points and their 'inner' control poiints. Coming from GDI+ GraphicsPath I see a sequence of points, altenrating between 1 anchor and 2 control points. Imo, this makes an even stronger case for treating the two control points as properties of the anchor point. Since both must be movable they may have a common ancestor or be hooked up to movecontroller class; but I trust you know best how to do that in Unity..
The property the question really started with was something like bool IsContinuous. When true we need to couple
moving a control point to moving the other one in 'the opposite' way.
moving the anchor to moving both control points in parallel
Maybe a property bool IsLocked to prevent moving it
Maybe a property bool IsProtected to prevent removing it when reducing/simplifying the curve. (Which is hardly needed for constructed curves but very much so for curves from free-hand drawing or tracing with the mouse)
Maybe a property to know that the point in a group of points which can be edited together.
Maybe a general marker.
Maybe a text annotation
Maybe a type indicator that denotes a break/split in the curve.
Maybe methods to increase or decrease smoothness vs. pointiness.
Some use cases clearly mostly involve the curve but others don't; and some are useful for both.
So, clearly we have a lot of good reasons to create a clever ÀnchPoint` class..
((I'm a bit tied up but still plan to write my own editor for the GraphicsPath bezier curves. If and when this happens, I'll update the post with things I learned including the class design I come up with..))
Hi all I am trying to create a game, and right now, I have a balloon class, which I want to currently create an amount of them via an array, and then draw then into the game.
Currently however it seems it is either only creating one instance of balloon or only drawing 1 instance. I'v looked over it multiple times, and from what I understand it should be looping through the entire array size (currently set at 10) and creating that many balloons, then updating and drawing that many.
Are you not able to do what I am currently doing with an array? Would I have to create each object sparately?
Here is the code of the two classes concerned.
GameLevel:
The class that creates the onjects and draws them.
public partial class GameLevel : GameScreen
{
SpriteBatch spriteBatch;
protected Game game;
Texture2D balloonTexture;
Balloon[] balloons = new Balloon[10];
public GameLevel(Game game, SpriteBatch spriteBatch)
: base(game, spriteBatch)
{
this.game = game;
this.spriteBatch = spriteBatch;
}
public void LoadContent()
{
for (int i = 0; i < balloons.Length; i++)
{
int colour;
Random random = new Random();
colour = random.Next(1, 6);
switch (colour)
{
case 1: balloonTexture = Game.Content.Load<Texture2D>("Images/BlueBalloon");
break;
case 2: balloonTexture = Game.Content.Load<Texture2D>("Images/RedBalloon");
break;
case 3: balloonTexture = Game.Content.Load<Texture2D>("Images/YellowBalloon");
break;
case 4: balloonTexture = Game.Content.Load<Texture2D>("Images/GreenBalloon");
break;
case 5: balloonTexture = Game.Content.Load<Texture2D>("Images/PurpleBalloon");
break;
}
balloons[i] = new Balloon(new Rectangle(0, 0, 1152, 648), balloonTexture);
balloons[i].SetStartPosition();
}
}
public void Update()
{
for (int i = 0; i < balloons.Length; i++)
{
balloons[i].Update();
}
}
public override void Draw(GameTime gameTime)
{
for (int i = 0; i < balloons.Length; i++)
{
balloons[i].Draw(spriteBatch);
}
base.Draw(gameTime);
}
}
}
Ballon Class:
public class Balloon
{
Vector2 position;
Vector2 motion;
Rectangle bounds;
Rectangle screenBounds;
public Texture2D texture;
float balloonSpeed = 4;
public Balloon(Rectangle screenBounds, Texture2D texture)
{
this.texture = texture;
this.screenBounds = screenBounds;
}
public Rectangle Bounds
{
get
{
bounds.X = (int)position.X;
bounds.Y = (int)position.Y;
return bounds;
}
}
public void Update()
{
position += motion * balloonSpeed;
}
private void CheckWallColision()
{
if (position.X < 0)
{
position.X = 0;
motion.X *= -1;
}
if (position.X + texture.Width > screenBounds.Width)
{
position.X = screenBounds.Width - texture.Width;
motion.X *= -1;
}
if (position.Y < 0)
{
position.Y = 0;
motion.Y *= -1;
}
if (position.Y + texture.Height > screenBounds.Height)
{
position.Y = screenBounds.Height - texture.Height;
motion.Y *= -1;
}
}
public void SetStartPosition()
{
Random rand = new Random();
motion = new Vector2(rand.Next(2, 6), -rand.Next(2, 6));
motion.Normalize();
position = new Vector2(rand.Next(100, 500), rand.Next(100, 500));
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, position, Color.White);
}
}
}
I believe the reason this is occurring is related to the way the Random object works. When one is constructed, it uses as its seed (from which it will generate all values) the system clock. This means that, if you generate numerous Random instances at the same time, every single one of them will return the same series of values. If you're using these values for positions, every single one of your objects will have the same position, and will be drawn over each other. In your case, because they're all the same size, it looks like only one was ever drawn.
To fix this, create a single instance of Random and just use Next() on that single instance whenever you need a new position.
Personally, I consider this a pretty significant design flaw - it doesn't seem amazingly intuitive that a series of Random objects created at the same time will be "random" in the exact same way. But: this is a good example of the fact that "random" values created by C#, and any other language, will in fact be pseudo-random, and when it comes to pseudo-randomness, implementation is very important.
Firstly, I would like to say sorry for making another thread asking the same question (more or less), but I am desperate for an answer and the other thread went dead.
I am currently working on a project for school in which I am attempting to make a simple 2d shooter. Problem is, I don't know how to implement collision detection between my two lists (being bullets and enemies as stated in title). I am very new to programming and have been using various tutorials as a guide, some tutorials say to use a nested for loop and if statement while I have seen others using an if statement, oh and I'm using the rectangle collision method not the pixel by pixel one I think it was called. Thanks in advance! :)
Here is my code:
Main:
namespace Software_Design_Major_Project
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D ship; // Declaring the sprite
Vector2 shipposition = Vector2.Zero; // Creating a vector for the sprite.
List<bullets> bullets = new List<bullets>(); // a list is created here so that there can be multiple copies of this item at once without writing the extra code.
Texture2D texture;
KeyboardState pastkey;
//bullet sound effect.
SoundEffect effect;
List<enemies> Enemies = new List<enemies>();
Random random2 = new Random();
float spawn = 0;
public enum GameState
{
MainMenu,
Playing,
}
GameState CurrentGameState = GameState.MainMenu;
int screenWidth = 600, screenHeight = 480;
cButton play;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
//changes width of screen to 600px.
graphics.PreferredBackBufferWidth = 600;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
// This statement positions the ship.
ship = Content.Load<Texture2D>("ship"); // Loads the ship into the memory.
shipposition = new Vector2((graphics.GraphicsDevice.Viewport.Width / 2) - (ship.Width / 2), 420);
// loads bullet sprite
texture = Content.Load<Texture2D>("bullet");
graphics.PreferredBackBufferWidth = screenWidth;
graphics.PreferredBackBufferHeight = screenHeight;
graphics.ApplyChanges();
IsMouseVisible = true;
play = new cButton(Content.Load<Texture2D>("play"), graphics.GraphicsDevice);
play.setPosition(new Vector2(225, 220));
effect = Content.Load<SoundEffect>("laser");
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
spawn += (float)gameTime.ElapsedGameTime.TotalSeconds;
// spawns enemy every second.
foreach (enemies enemy in Enemies)
enemy.update(graphics.GraphicsDevice);
MouseState mouse = Mouse.GetState();
switch (CurrentGameState)
{
case GameState.MainMenu:
if (play.isClicked == true)
CurrentGameState = GameState.Playing;
play.Update(mouse);
break;
case GameState.Playing:
if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.E))
{
Exit();
}
break;
}
// Allows the ship to move left and stops the ship going off the screen.
if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Left) && shipposition.X >= 0)
{
shipposition.X -= 7;
}// same as above except for the right direction.
if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Right) && shipposition.X < ((graphics.GraphicsDevice.Viewport.Width) - (ship.Width)))
{
shipposition.X += 7;
}
// this prevents the player from holding down space to spam bullets.
if (Keyboard.GetState().IsKeyDown(Keys.Space) && pastkey.IsKeyUp(Keys.Space))
{
bullets bullet = new bullets(texture);
// the ships coordinates are gathered from the top left hand corner of the sprites rectangle therefore 'new Vector2(shipposition.X + 32, shipposition.Y)' had to
// be added rather than just = 'shipposition' to avoid having the bullets shoot from the wing.
bullet.position = new Vector2(shipposition.X + 32, shipposition.Y);
bullets.Add(bullet);
effect.Play();
}
pastkey = Keyboard.GetState();
//calls upon the update method from the bullets class.
foreach (bullets bullet in bullets)
bullet.update();
LoadEnemies();
base.Update(gameTime);
}
public void LoadEnemies()
{
int randX = random2.Next(10, 540);
if (spawn <= 1)
{
spawn = 0;
//limits amount of enemies on screen to 5.
if (Enemies.Count() < 5)
Enemies.Add(new enemies(Content.Load<Texture2D>("enemy"), new Vector2(randX, -100)));
}
for (int i = 0; i < Enemies.Count; i++)
{
if (!Enemies[i].enemyVisble)
{
//removes the enemies when they go off screen.
Enemies.RemoveAt(i);
i--;
}
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// calls draw method in bullets class
foreach (bullets bullet in bullets)
{
bullet.Draw(spriteBatch);
}
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend);
spriteBatch.Draw(ship, shipposition, Color.White); // draws ship sprite
switch (CurrentGameState)
{
case GameState.MainMenu:
play.draw(spriteBatch);
spriteBatch.Draw(Content.Load<Texture2D>("menu"), new Rectangle(0, 0, screenWidth, screenHeight), Color.White);
break;
case GameState.Playing:
break;
}
foreach (enemies enemy in Enemies)
{
enemy.draw(spriteBatch);
}
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Bullets Class:
namespace Software_Design_Major_Project
{
class bullets // A new class needs to be created to allow for bullets.
{
public Texture2D texture;
public Vector2 position;
public bool isvisible;
public bullets(Texture2D newtexture)
{
texture = newtexture;
isvisible = false;
}
public void update()
{
position.Y -= 3; // velocity of the bullet
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Begin();
spriteBatch.Draw(texture, position, Color.White);
spriteBatch.End();
}
}
}
Enemies Class:
namespace Software_Design_Major_Project
{
public class enemies
{
public Texture2D enemyTexture;
public Vector2 enemyPosition;
public bool enemyVisble = true;
public float enemyMoveSpeed;
public int Value;
Random random = new Random();
int randY;
public enemies (Texture2D newEnemyTexture, Vector2 newEnemyPosition)
{
enemyTexture = newEnemyTexture;
enemyPosition = newEnemyPosition;
randY = random.Next(1, 4);
enemyMoveSpeed = randY;
enemyVisble = true;
Value = 100;
}
public void update(GraphicsDevice graphics)
{
enemyPosition.Y += enemyMoveSpeed;
if (enemyPosition.Y > 500)
enemyVisble = false;
}
public void draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(enemyTexture, enemyPosition, Color.White);
enemyVisble = true;
}
}
}
You have several ways to do it... one of them maybe add a radio property to enemies and bullets....
for (int bi=0; bi<bullets.count; )
{
bool should_destroy_bullet = false;
Bullet b = bullets[bi];
for (int ei=0; ei<enemies.count; )
{
Enemy e = ememies[ei];
if (b.radio + e.radio < Vector2.Distance(b.Pos, e.Pos)) // Check collision
{
e.Died();
enemies[ei] = enemies[enemies.Count-1];
enemies.RemoveAt(enemies.Count-1);
should_destroy_bullet = true; // This lets a bullet kill more than one enemy
} else ei++;
}
if (should_destroy_bullet) {
b.Died();
bullets[bi] = bullets[bullets.count-1];
bullets.RemoveAt(bullets.count-1);
} else bi++;
}
Or you can build a rectangle for each sprite and check if they intersects....
if (b.Bounds.Intersects(e.Bounds)) ....
The Pythagorean theorem could work for you.(A squared plus B squared = C squared)
Make your players and enemies have a radius.Put this in your constructor.
public float radius = 15;
Put the following in a update void/subclass so that it happens every second.
Make a float the X location of your player and subtract the X location of the bullet. Square it.
float XDist = Math.Pow(player1.location.X - bullet.location.X,2);
Make a float the Y location of your player and subtract the Y location of the bullet.Square it.
float YDist = Math.Pow(player1.location.X - bullet.location.X,2);
Make a float to figure out the square root of the sum of Xdist and Ydist.
float actualDist = Math.Sqrt(Xdist + Ydist);
Now if the radius of your player is less than the distance between the player and the bullet, the bullet has hit the player.
if(actualDist < radius)
bullethit == true;