Add a new orange ring entity to the scene. Make it move to the right at a constant speed. When it reaches the right edge of the screen, make it bounce back to the left.
Important! The rigidbody component is the hook into Unity's built-in physics engine. We are not using that in this assignment (doing it ourselves one time), so there should be no rigidbody components in your project.
Collision detection is the process of determining if any elements in the game are touching each other. In this simple case, we just want to know if the ring is touching the right wall, which is a very simple calculation for a circle and a vertical line.
During the update when the circle touches the wall, collision response involves two actions:
To do all this, you need to know the position of the wall, the position of the ring, and the size (radius) of the ring sprite.
Script component: Add a new MonoBehaviour script component called Bounce
to the entity that controls its movement by setting position on each Update. Movement
should be controlled by a velocity vector which you integrate over time to get the new position. As we saw in the last assignment, velocity can be updated a number of ways,
including based on user input. In this case, you're just setting it to the right at some constant speed.
Camera.main.ViewportToWorldPoint: This Unity method converts viewport coordinates to world coordinates. The viewport uses normalized coordinates
(0, 0)
to (1, 1)
. Converting (1, 1)
will tell you the world coordinates of the top right corner of the visible game area.
GetComponent<SpriteRenderer>(): This Unity method is available to every MonoBehaviour to access the SpriteRenderer component on your game object. The
SpriteRenderer has a property sprite.bounds.extents
that gives you the half-width and half-height of the sprite, useful for collision detection calculations.
Instead of hardcoding the starting direction, generate a random direction vector each time the game starts. Extend your single-wall collision detection and response to check all four walls. There's nothing fancy about that, you just check all four.
You will need to update your collision response to deal with collisions that are not exactly perpendicular to the wall. Fortunately, this is simple with horizontal and vertical walls (called axis-aligned). A surface can only extert force in the perpendicular direction, so a vertical wall can only change the x component of the velocity, and a horizontal wall can only change the y component. On collision, you flip the affected component (x or y) and keep the other one the same.
Random.Range: Unity's built-in method for generating random numbers. Use Random.Range(-1f, 1f)
to create random x and y velocity components.
Vector2.normalized: Vector normalization maintains the direction but changes the magnitude to be 1.0f. Apply this after creating a random vector to get a unit vector (length 1.0f) in a random direction. That direction vector can then be multiplied by a constant speed.
All code should generlize over data. When you have multiple entities that behave the same way, the same script should work for all of them. Components are classes. When the game starts and an entity is created, it instantiates a new object for each component class attached to it. Like any object, they have their own private copies of all their internal variables.
In this case, we want to create three rings, each with the same script component, moving in different directions. This demonstrates a common game design pattern that Unity uses: component-based design. The same script component can be attached to multiple game objects, and each will maintain its own state (position, velocity, etc.) while sharing the same behavior logic.
Prefabs: Instead of manually creating three entities and attaching the script to each, use Unity's template system for creating reusable game objects. Create a prefab by dragging your configured circle (with sprite and script) from the scene into the Project window. You can then drag the prefab back into the scene to create identical copies, each with their own independent behavior. Prefab elements in the scene are linked back to the prefab so you can make changes to them all at once.
Make the three rings appear at different times in random places. To do this, we're going to start them off-screen with updating disabled. Every 3 seconds, activate a ring by moving it into the visible area and enabling update.
This is a common technique called pooling. We could dynamically create each ring when it is needed, but creating and destroying objects is expensive and can lead to uneven performance during gameplay (i.e. lagging). Most games use loading time to pre-create entities and move them in and out as needed.
Parenting: In the scene hierarchy on the right, entities can be parented to each other (drag and drop) creating a tree. This is useful for positioning and movement relative to your parent, for keeping track of a set of entities
(the children of a certain entity), and for keeping the hierarchy neat. Create an empty
entity called RingPool
, position it offscreen, and parent your three rings to it. Notice that they don't move in the scene
when parented. Instead, their transform.position changes relative to the location of their parent. (Try move the pool around and see.) Set all their starting positions to (0,0,0) and they'll move to the parent.
MonoBehaviour.enabled: You can enable/disable a script using the checkbox in the inspector pane in the Unity editor. Programmatically, you GetComponent<Bounce>
to get the script and set its .enabled
to True
or False
. Disabling means that Update doesn't run. You can also enable/disable the entire entity using SetActive
.
Note: enabling a script does not run Start, it runs only once which is fine in this case. There are other methods to override such as OnEnable if you need to run something on every enable. See documentation here.
Timer: Timers are ubiquitous in games. It's just a variable that you update using Time.deltaTime
every frame until it reaches a value. You can count up or down. Add a Spawner script component to your RingPool
and set up a timer to print a debug message every 3 seconds.
Transform.childCount / Transform.GetChild: Parenting is actually done through the Transform component rather than the entity itself. These properties and methods let you access and manage the list of children through your transform. When your timer goes off, you can check if you have any more child rings and if so, activate the next one.
Transform.parent: You can programmatically change the parent of a Transform by setting it to another Transform or to null
(no parent).
e.g. transform.parent = null;
or transform.parent = someEntity.transform;
.
To spawn the next ring from the Spawner script on RingPool: