Difference between revisions of "Basic Scripting"
m (→UI) |
m (→Should You Use Enter, Stay, or Exit?) |
||
(One intermediate revision by the same user not shown) | |||
Line 175: | Line 175: | ||
We can start off by tagging both spheres as 'Spheres', and making sure they have collider and rigid body components. | We can start off by tagging both spheres as 'Spheres', and making sure they have collider and rigid body components. | ||
− | We should then initialize some variables within the class MonoBehavior curly braces. Like last time, we want intensity and color to be able to be chosen from the Inspector. We also still need access to the light component. ''You can use the same method as before to switch between old colors and new, but with collisions, that might cause the light to flicker weirdly so fiddling with | + | We should then initialize some variables within the class MonoBehavior curly braces. Like last time, we want intensity and color to be able to be chosen from the Inspector. We also still need access to the light component. ''You can use the same method as before to switch between old colors and new, but with collisions, that might cause the light to flicker weirdly, so fiddling with this implementation may be necessary for better visuals.'' |
public float intensity; | public float intensity; | ||
Line 195: | Line 195: | ||
=UI= | =UI= | ||
Let's finally work on making that pause menu actually pause the game! | Let's finally work on making that pause menu actually pause the game! | ||
− | The only variables we need to save are the main ui canvas | + | The only variables we need to save are for the main ui canvas game object, but I also made some text change based on whether the game was paused or not. |
− | + | In the start method you should find the ui game object (if you didn't choose to have it be a public variable you drag into the inspector), and set it to be inactive, so it doesn't show up until the player presses a button. | |
− | + | This next part is about coroutines, and that just means every time the function, or coroutine, goes through its code until it reaches a yield statement, at which point it leaves the coroutine and continues functioning as normal past that, and then the coroutine resumes from where it left off, all the variables and methods in the same place, until it reaches another yield statement. For a more detailed overview of coroutines, refer to https://docs.unity3d.com/Manual/Coroutines.html. The function declaration of a coroutine always starts with IEnumerator as what is being returned, this is to keep track of everything properly. | |
+ | So we'd want to use StartCoroutine(name_of_your_coroutine_function); in the start function to kick things off properly. | ||
+ | |||
+ | Then for the coroutine itself, to have it continually check for the player wanting to open the menu, we use while(true){} | ||
+ | There is no case where this while statement is false so it keeps going forever, which is good for a pause menu, but that might not be the case with other things you'd use a coroutine for. | ||
+ | So every frame this coroutine is called, we ask, has the escape key been pressed? If so is the game already paused (in which case the Time.timeScale is 0), and in that case make the pause menu go away, the cursor disappear and make time run like normal again, by setting Time.timescale to 1. If timeScale is not 0, then do the opposite, as in make the ui show up, the cursor visible, and freeze the Time.timeScale by setting it to 0. After all that we can do a 'yield return null;' to leave the coroutine. | ||
+ | |||
+ | |||
+ | The complete code if you need it: | ||
+ | GameObject ui; | ||
− | + | void Start () { | |
− | |||
ui = GameObject.FindGameObjectsWithTag("ui")[0].gameObject; | ui = GameObject.FindGameObjectsWithTag("ui")[0].gameObject; | ||
ui.SetActive(false); | ui.SetActive(false); | ||
− | StartCoroutine(PauseCoroutine()); | + | StartCoroutine(PauseCoroutine()); |
− | + | } | |
− | + | IEnumerator PauseCoroutine () { | |
while(true){ | while(true){ | ||
if(Input.GetKeyDown(KeyCode.Escape)){ | if(Input.GetKeyDown(KeyCode.Escape)){ | ||
Line 217: | Line 225: | ||
ui.SetActive(true); | ui.SetActive(true); | ||
Cursor.visible = true; | Cursor.visible = true; | ||
− | |||
− | |||
Time.timeScale = 0; | Time.timeScale = 0; | ||
} | } | ||
Line 224: | Line 230: | ||
yield return null; | yield return null; | ||
} | } | ||
− | |||
− | |||
− | |||
− | |||
} | } | ||
} | } |
Latest revision as of 10:02, 7 September 2017
*Work in progress page!*
With Unity you can use javascript, c#, or boo, and while the 3 are somewhat similar, there are some major differences. I'm only going to talk about c# here as that is what most Unity users use and the language you will find the most helpful online answers for. If you don’t already know C#, please go learn it first. Here's a good c# tutorial. This scripting guide can be skipped to come back to later, but it is highly suggested that you go through this material at some point.
To create a new script go to your project tab and right click anywhere then Create > C# script. Name it whatever you want, as long as you can remember it. You can also add a script onto an object by using 'Add Component' and either finding or typing in 'New Script'. Scripts actually can only ever affect things in the game if they are attached to an object. Any script you make can be found by searching when using 'Add Component' or dragged onto the object in Hierarchy or when an object is in the inspector.
Say we want a specific cube to be half its size when we press the 'h' button in-game. In that case we first need to know which cube to resize, you can do that either by having the script be a component of the cube in question, in which case you don't need to do anything else to know which cube, or you can have the object be a variable you pick.
Contents
Detecting keyboard input
To check whether the H key has been pressed say
if(Input.GetKeyDown('H')){ print("H key has been pressed!"); }
This will make a "H key has been pressed!" message appear in the Console tab upon the first frame an 'H' key is pressed (so it only does the thing between the curly braces once for every time the user presses the button).
Vector and transform manipulation
Continuing with the example from the previous section, to then resize that cube on that signal, you would change the code within the curly braces of the if statement to:
- if the script will be going on the cube:
transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
And you don't have to do much else besides make sure the script is on the proper game object, of course.
- To change the cube if the script is a component of some other object that isn't the cube, your code would look more like this:
The declaration of cube and its placement before Start() is important so it has the right scope for it to a known variable in both Start() and Update().
The script can then be placed on any object in the scene, but you will have to tell it what cube is.
Timing
Start
Start() is often where you initialize variables, or where you can call a function if you only want it to happen once at the start of play.
Update
Update() is called once per frame, and is probably where the bulk of your code will be, or at least where it gets called.
Awake
Awake() is called right before Start() or after the object the script is on has been instantiated. If the object is deactivated at startup, Awake() is still called when, or if, the object is set to active.
Activation/ deactivation can be toggled in the inspector.
It can also be toggled via script by saying {object}.SetActive(true); or {object}.SetActive(false);, and {object} must be of type GameObject.
FixedUpdate
Most physics-related updates should happen in here because physics changes are done directly after FixedUpdate(). FixedUpdate() is also different from Update() in that it happens at a fixed rate while Update() varies based on the framerate the game is currently running at.
For more information visit Order of execution
Getting components and their attributes from Game Objects
Say you want to change the color and intensity of a light, but only when the player is within a certain distance of it. The easiest way to do that would be to write a small script and attach it to the light object. We'll start with finding out all the components and variables the script needs to know about to achieve this, and that would include the player object, the threshold distance between player and light for when you want the light to change, and the new values you'd like the light to have. Saying public in the declaration makes it so those variables can be given values in the inspector. The script will be on the light object for this example, so we don't need a declaration for that. However, we do probably a variable for the light component of the light object because although it can be gotten through a function call multiple times, it's probably easier to only call that function once and save the result to a variable. Lastly we declare a variable to save the distance between the player and the light, that will be changed in update so it doesn't need to be initialized yet.
public GameObject player; public float intensity; public Color color; public float threshhold; Light lightComponent; float distance;
Those last two variables don't need to be public because they won't be looked at by other scripts or given value in the inspector. For lights, range and intensity are both floats, and in Unity at least, Color is a type. To get the light component you have to use GetComponent<Light>(); on the game object, but if its just on the object the script is attached to, you don't actually have to specify that GameObject. Set that equal to your lightComponent variable in the Start() function.
The update function is gonna be where some action is finally happening. First we have to see what the distance between player and light is. Position has the type of a Vector3 (or a vector in the 3rd dimension) so we can use the Vector3.Distance() function to easily figure out the math for us and the function returns a float so we can save it as our distance variable that we declared earlier. Now we can check if the distance is below the threshold, and if it is, make all those changes to things in the light component. All the attributes happen to be named the same things as the variables I made, but they absolutely do not have to be.
void Update () { distance = Vector3.Distance(player.transform.position, transform.position); if(distance < threshold){ lightComponent.range = range; lightComponent.intensity = intensity; lightComponent.color = color; } }
One problem that you will probably see with this code when you try it out is that once you get into range and then leave, the light stays the same, it doesn't go back to how it looked before! Let's fix that.
First we need to add two variables to hold the original values, like
float origIntensity; Color origColor;
Then we have to initialize those values and the best place for that would be in Start() so it's only done once.
void Start () { lightComponent = GetComponent<Light>(); origIntensity = lightComponent.intensity; origColor = lightComponent.color; }
The last step is to change the color back, our intended result, so we add an else to the end of the if statement we originally had checking the different between distance and threshold.
else if(lightComponent.color != origColor){ lightComponent.intensity = origIntensity; lightComponent.color = origColor; }
You also can't really see the change in light very well unless you attach the light to an object, especially if you are using a point light.
Empty Game Objects
They're actually kind of useful! You can put scripts on them and make it a kind of "manager" of the rest of the objects in the scene. So all the scripts can be in one place if you want and that might make things easier. To make one go to the GameObject menu (by right-clicking or finding it in the top toolbar) and choose "Create Empty". It just looks like a point in space when you first place it down, but it can be used as an otherwise featureless container for many different objects that you want grouped together. Or as I said before, it can be used as a scripting "manager" of many scripts that affects other objects in the scene. It can even be both of those, if you want. Lastly, you may find it useful to know that you can attach visual tags to objects to make it easier to see different things in the scene view. Visual tags, or icons can be added by clicking on the colorful square next to an object's name in the inspector view and choosing an icon. The name of the object shows up when you use the long bars, doesn't if you use one of the smaller icons.
Tags
Game Object tags are useful as it lets you filter between objects based on keywords you can choose, and they don't mess with the object name as the tag is a separate property of the object. Add as many tags as you want, and then when you're in a script and have access to any game object, you can check its tag by using {Game Object variable name}.tag. Or you can search the entire scene for objects with that tag, and they'll all be returned as a list of objects.
GameObject.FindGameObjectsWithTag("tag word in quotes");
By using "GameObject" above, the scope of the search is the entire scene, but you can make the scope smaller by choosing a particular GameObject variable and replacing "GameObject" with its variable name.
Keep in mind that all GameObjects are a large set of properties and components, some of them first level, like transform and tag, while others are deeper, like position or component properties.
Instantiation
Let's say we want to make a cube with some certain properties every time the player presses space (or jump). The first step in achieving this is to make a cube with the properties we want, like the material, collider, rigid body, etc. For this example, we'll use a cube with a default collider and rigid body, a nice blue material, make a tag for cube and apply it, and lastly let's rename it to be 'CoolCube'. You should also tag the player object with a 'Player' tag.
Next, we have to make a prefab based on that cube. That can be done by dragging your cube from the hierarchy into the project tab. You can actually delete the original cube from the scene now if you want to.
This next step is where the coding comes in: pick an object you'd like to attach the script you'll be making, doesn't really matter which. open a new script file and for variables, you're going to need the prefab for the cube, which should be public, and the player object, since we gave the player the 'Player' tag, we can find it with a simple piece of code rather than the normal way with public. This is the you'll eventually get to do those things:
{ public GameObject cube; Transform player; Vector3 pos; // Use this for initialization void Start () { player = GameObject.FindGameObjectsWithTag("Player")[0].transform; } // Update is called once per frame void Update () { if(Input.GetKeyDown(KeyCode.Space)){ pos = new Vector3(player.position.x, player.position.y -cube.transform.lossyScale.y - 0.1f, player.position.z); GameObject newCube = GameObject.Instantiate(cube, pos, player.rotation); } } }
The part in Start() means the player transform variable is set equal to the transform of first result from searching the whole scene for objects with the tag 'Player' (all searches like this return an array, even if they are only of length 1, as it should be here, only one object in your scene should have the 'Player' tag).
In the Update function, we first check if the player is pressing space, because otherwise we don't want this script to be doing anything. Next, we want to figure out where exactly the new cube should go before even considering instantiating it. Here, we make it spawn at the same x and z as the player, and just below the player. 'cube.transform.lossyScale.y' means get the cube global y-scale, just in case we want to scale up the cube in the future and have this script still work. The extra 0.1f makes sure there's a little extra room between the player's jump and the cube so that as the player continues going upward with their jump, they don't hit the cube and go slightly more forward than intended.
And finally we get to the line for instantiating the cube. You can instantiate things other than GameObjects, or more your own GameObject classes, but since we're using a normal GameObject we should say that so it's more clear what is being instantiated. The three parameters for the function are the prefab to use, the position at which to place the new object, and the rotation of the new object.
We should set it equal to another variable if we want to do something to it later in this script, like changing its name. You'll notice when you try the game out with this script running from some object, that the instantiated cubes appear in the hierarchy as 'CoolCube(Clone)'. After the object has been instantiated, you can change this name to whatever you want by saying
newCube.name = 'Whatever you want to name it';
You could also set the new cube's parent object so they all get grouped together and don't clutter the hierarchy. To do so, you'd want to make another public variable for the parent Transform (which you can get by just dragging the GameObject over into that spot for the variable in Inspector, it works) you'd like to use.
Then use
newCube.transform.setParent({parent transform variable here});
Colliders and Collision Detection
Say you want something to happen only after the object collides with something, or maybe you want it to do something different depending on what it collides with. That's where the OnCollisionStay(), OnCollisionEnter(), and OnCollisionExit() events come in useful.
Should You Use Enter, Stay, or Exit?
Enter is activated on the first frame two objects with colliders hit each other, and only that frame until the objects come apart and meet again.
Stay is called for every frame that two colliders are still hitting each other.
Exit is like Enter, except it's activated the frame the objects move outside of each other's collider bounds.
We'll use Enter because in this case the change only needs to be done once per collision. However, if we were trying to do something requiring user input and collisions at the same time, using Stay is better; otherwise the user must do their input at exactly right frame, which shouldn't be the requirement.
So let's say you want to make the ball from our earlier example only change color when something hits it, but you want that something to only be another ball, not the player or the terrain.
We can start off by tagging both spheres as 'Spheres', and making sure they have collider and rigid body components.
We should then initialize some variables within the class MonoBehavior curly braces. Like last time, we want intensity and color to be able to be chosen from the Inspector. We also still need access to the light component. You can use the same method as before to switch between old colors and new, but with collisions, that might cause the light to flicker weirdly, so fiddling with this implementation may be necessary for better visuals.
public float intensity; public Color color; Light lightComponent;
Start() should just have:
lightComponent = GetComponent<Light>();
We don't need an Update() function for this script, so you can delete that and replace it with OnCollisionEnter(Collision col){}. Collision is a class for holding all the collision related information, like the velocities of the two objects hitting each other, the other collider that was hit and its game object.
Now inside that function, let's first check if the game object whose collider was hit has the right tag. If it does, we should change the light's color to that new color and intensity.
if(col.gameObject.tag == "sphere"){ lightComponent.intensity = intensity; lightComponent.color = color; }
UI
Let's finally work on making that pause menu actually pause the game! The only variables we need to save are for the main ui canvas game object, but I also made some text change based on whether the game was paused or not.
In the start method you should find the ui game object (if you didn't choose to have it be a public variable you drag into the inspector), and set it to be inactive, so it doesn't show up until the player presses a button. This next part is about coroutines, and that just means every time the function, or coroutine, goes through its code until it reaches a yield statement, at which point it leaves the coroutine and continues functioning as normal past that, and then the coroutine resumes from where it left off, all the variables and methods in the same place, until it reaches another yield statement. For a more detailed overview of coroutines, refer to https://docs.unity3d.com/Manual/Coroutines.html. The function declaration of a coroutine always starts with IEnumerator as what is being returned, this is to keep track of everything properly. So we'd want to use StartCoroutine(name_of_your_coroutine_function); in the start function to kick things off properly.
Then for the coroutine itself, to have it continually check for the player wanting to open the menu, we use while(true){} There is no case where this while statement is false so it keeps going forever, which is good for a pause menu, but that might not be the case with other things you'd use a coroutine for. So every frame this coroutine is called, we ask, has the escape key been pressed? If so is the game already paused (in which case the Time.timeScale is 0), and in that case make the pause menu go away, the cursor disappear and make time run like normal again, by setting Time.timescale to 1. If timeScale is not 0, then do the opposite, as in make the ui show up, the cursor visible, and freeze the Time.timeScale by setting it to 0. After all that we can do a 'yield return null;' to leave the coroutine.
The complete code if you need it:
GameObject ui; void Start () { ui = GameObject.FindGameObjectsWithTag("ui")[0].gameObject; ui.SetActive(false); StartCoroutine(PauseCoroutine()); } IEnumerator PauseCoroutine () { while(true){ if(Input.GetKeyDown(KeyCode.Escape)){ if(Time.timeScale == 0){ ui.SetActive(false); Cursor.visible = false; Time.timeScale = 1; }else{ ui.SetActive(true); Cursor.visible = true; Time.timeScale = 0; } } yield return null; } } }