为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

创建固定视角滑动游戏

2018-04-13 50页 doc 722KB 11阅读

用户头像

is_421808

暂无简介

举报
创建固定视角滑动游戏创建固定视角滑动游戏 introduction In this tutorial we'll create a very simple endless running game. You'll learn to generate a layered background; reuse objects; use simply physics; detect input to make the player jump; implement a power-up; write a small event manager...
创建固定视角滑动游戏
创建固定视角滑动游戏 introduction In this tutorial we'll create a very simple endless running game. You'll learn to generate a layered background; reuse objects; use simply physics; detect input to make the player jump; implement a power-up; write a small event manager; switch stuff on and off on demand; make a minimal GUI. You're assumed to know your way around Unity's editor and know the basics of creating C# scripts. If you've completed the Clock tutorial you're good to go. The Graphs tutorial is useful too, but not necessary. Game Design Before we get started, we should make some decisions about what we put in the game. We're going to make a very simple 2D side-scroller, but that's still very vague. Let's narrow it down a bit. For gameplay, we'll have a runner who dashes towards the right of the screen. The player needs to jump from platform to platform for as long as possible. These platforms can come in different flavors, slowing down or speeding up the runner. We'll also include a single power-up, which is a booster that allows mid-air jumps. For graphics, we'll simply use cubes and standard particle systems. The cubes will be used for the runner, power-up, platforms, and a skyline background. We'll use particle systems to add a trail effect and lots of floating stuff to give a better sense of speed and depth. There won't be any sound or music. Setting the Scene Open a new project without any packages. The default 2 by 3 editor layout is a good one for this project, but you can use whatever layout you prefer. Let's make this game with a 16:10 display ratio in mind, so select this option in the Game view. Our game is basically 2D, but we want to keep a little feeling of 3D. An orthographic camera doesn't allow for 3D, so we stick to a perspective camera. This way we can also get a multilayered scrolling background by simply placing stuff at various distances. Let's say the foreground is at depth 0 and we have a background layer at depth 50 and another one at depth 100. Let's place three cubes at these depths and use them as guides to construct the scene. I went ahead and picked a view angle and color setup, but you're free to experiment and choose whatever you like. Add a directional light (GameObject / Create Other / Directional Light) with a rotation of (20, 330, 0). This gives us a light source that's shining over our right shoulder. Because it's a directional light its position doesn't matter. Reduce the Field of View of the Main Camera to 30, position it at (5, 15, -40), and rotate it by (20, 0, 0). Also change its Background color to (115, 140, 220). Create three cubes (GameObject / Create Other / Cube) with Z positions of 0, 50, and 100. Call them Runner, Skyline Close, and Skyline Far Away respectively. Remove the collider from both skyline cubes, because we won't be needing those. Create a material for each in the Project view via Create / Material, naming them Runner Mat and so on, then assign them to the cubes by dragging. I used default diffuse shaders with the colors white, (100, 120, 220), and (110, 140, 220). To keep things organized, create a Runner and a Skyline folder in the Project view (Create / Folder) and put the materials in there. Running So far it doesn't look like much and nothing's happening yet, but that will change. Let's start by creating a mock-up of the game in action by instructing the Runner to move to the right. Create a new C# script called Runner inside the Runner folder and attach it to our Runner cube. Write the following code to make it move. using UnityEngine; public class Runner : MonoBehaviour { void Update () { transform.Translate(5f * Time.deltaTime, 0f, 0f); } } While it's not much, it already shows us a problem when entering play mode. The camera does not follow the cube. To fix this, drag Main Camera onto Runner so is becomes a child of it. Now Runner remains at a fixed position in our view and we can see that the close skyline cube appears to move faster than the one further away. Generating a Skyline Now that we have rudimentary movement, let's generate a row of cubes to construct an endless skyline. The first thing we should realize is that only the visible part of the skyline needs to exist. As soon as a cube falls off the left side of the screen, it can be destroyed. Or better yet, reused to build the next part of the skyline that's about to enter view. We can program this behaviour by generating a queue of cubes and constantly moving the front cube to the back as soon as it's no longer visible. Create a new C# script in the Skyline folder and name it SkylineManager. We will use it to create two managers, one for each of the skyline layers. At minimum, it needs to know which prefab to use to generate the skyline, so let's start by adding a public variable for that. using UnityEngine; public class SkylineManager : MonoBehaviour { public Transform prefab; } To keep things organized, create a new empty object (GameObject / Create Empty) named Managers which we'll use as a container for all of our manager objects. Create another empty object named Skyline Close Manager, make it a child of Managers, and create a SkylineManager component for it by dragging the script on it. Now turn both skyline cubes into prefabs by dragging them into the Skyline project folder or via Create / Prefab and then dragging onto that. Afterwards, delete both cubes from the Hierarchy. Now drag the Skyline Close prefab onto the Prefab field of our Skyline Close Manager. We need a starting point from where we'll start spawning cubes. We can use the manager's own position for this. We also need to determine how many cubes we need to spawn to fill the screen. Let's simply use a variable named numberOfCubes for that. To keep track of where the next cube needs to spawn we'll use a private variable named nextPosition. using UnityEngine; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; private Vector3 nextPosition; void Start () { nextPosition = transform.localPosition; } } The next step is spawning the initial row of cubes. We'll use a simple loop for that, instantiating new objects, setting their position, and advancing nextPosition by the width of the object so they form an unbroken line. using UnityEngine; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; private Vector3 nextPosition; void Start () { nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Transform o = (Transform)Instantiate(prefab); o.localPosition = nextPosition; nextPosition.x += o.localScale.x; } } } Now set the position of Skyline Close Manager to (0, -1, 0) and set its Number of Objects field to 10. When entering play mode, we'll see a short row of cubes appear below Runner. However, once the cubes move out of view we'll never see them again. The idea is that we'll recyle objects once Runner has moved past them by some distance. For this to work, the manager must know how far Runner has traveled. We can provide for this by adding a static variable named distanceTraveled to Runner and making sure that it's always up to date. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; void Update () { transform.Translate(5f * Time.deltaTime, 0f, 0f); distanceTraveled = transform.localPosition.x; } } Now we'll store our skyline objects in a queue and keep checking whether the first object in it should be recycled. If so, we'll reposition it and move it to the back of the queue. Let's use a recycleOffset variable to configure how far behind Runner this reuse should occur. A value of 60 seems to work well. using UnityEngine; using System.Collections.Generic; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; private Vector3 nextPosition; private Queue objectQueue; void Start () { objectQueue = new Queue(numberOfObjects); nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Transform o = (Transform)Instantiate(prefab); o.localPosition = nextPosition; nextPosition.x += o.localScale.x; objectQueue.Enqueue(o); } } void Update () { if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){ Transform o = objectQueue.Dequeue(); o.localPosition = nextPosition; nextPosition.x += o.localScale.x; objectQueue.Enqueue(o); } } } This works! After entering play mode, you'll see the cubes reposition themselves. However, it doesn't look much like a skyline yet. To make it more like a real irregular skyline, let's randomly scale the cubes whenever they're placed or recycled. First, consider that both initially placing and later on recycling a cube is basically doing the same thing. Let's put this code in its own Recycle method and rewrite our Start and Updatemethods to both use it. using UnityEngine; using System.Collections.Generic; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; private Vector3 nextPosition; private Queue objectQueue; void Start () { objectQueue = new Queue(numberOfObjects); for(int i = 0; i < numberOfObjects; i++){ objectQueue.Enqueue((Transform)Instantiate(prefab)); } nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } } void Update () { if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){ Recycle(); } } private void Recycle () { Transform o = objectQueue.Dequeue(); o.localPosition = nextPosition; nextPosition.x += o.localScale.x; objectQueue.Enqueue(o); } } Next, we'll introduce two variables to configure the maximum and minimun allowed size and use them to randomly scale our objects. After picking a scale, we'll make sure to position the object so they're all aligned at the bottom. using UnityEngine; using System.Collections.Generic; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 minSize, maxSize; private Vector3 nextPosition; private Queue objectQueue; void Start () { objectQueue = new Queue(numberOfObjects); for(int i = 0; i < numberOfObjects; i++){ objectQueue.Enqueue((Transform)Instantiate(prefab)); } nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } } void Update () { if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){ Recycle(); } } private void Recycle () { Vector3 scale = new Vector3( Random.Range(minSize.x, maxSize.x), Random.Range(minSize.y, maxSize.y), Random.Range(minSize.z, maxSize.z)); Vector3 position = nextPosition; position.x += scale.x * 0.5f; position.y += scale.y * 0.5f; Transform o = objectQueue.Dequeue(); o.localScale = scale; o.localPosition = position; nextPosition.x += scale.x; objectQueue.Enqueue(o); } } To get a nice skyline effect, position the manager at (-60, -60, 50), set Min Size to (10, 20, 10), set Max Size to (30, 60, 10), and set Recycle Offset to 60. Let's go ahead and add the second skyline layer as well. Duplicate Skyline Close Manager and change its name to Skyline Far Away Manager. Change its Prefab to the Skyline Far Away prefab. Set its position to (-100, -100, 100), its Recycle Offset to 75, its Min Size to (10, 50, 10), and its Max Size to (30, 100, 10). Of course you can use any values you like instead. Generating Platforms Adding platforms to the game is basically doing the same thing as generating a skyline, with only a few differences. The elevation of the platforms needs to change at random and there need to be gaps between them. Also, we want to constrain the elevation of the platforms to make sure our skyline remains properly in view. If a platform is placed outside this range, we should bounce it back. Create a new folder in the Project view named Platform. Create a new C# script in there called PlatformManager and copy the code from SkylineManager into it. Then change the code as shown below to make if conform to our needs. using UnityEngine; using System.Collections.Generic; public class PlatformManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 minSize, maxSize, minGap, maxGap; public float minY, maxY; private Vector3 nextPosition; private Queue objectQueue; void Start () { objectQueue = new Queue(numberOfObjects); for(int i = 0; i < numberOfObjects; i++){ objectQueue.Enqueue((Transform)Instantiate(prefab)); } nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } } void Update () { if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){ Recycle(); } } private void Recycle () { Vector3 scale = new Vector3( Random.Range(minSize.x, maxSize.x), Random.Range(minSize.y, maxSize.y), Random.Range(minSize.z, maxSize.z)); Vector3 position = nextPosition; position.x += scale.x * 0.5f; position.y += scale.y * 0.5f; Transform o = objectQueue.Dequeue(); o.localScale = scale; o.localPosition = position; objectQueue.Enqueue(o); nextPosition += new Vector3( Random.Range(minGap.x, maxGap.x) + scale.x, Random.Range(minGap.y, maxGap.y), Random.Range(minGap.z, maxGap.z)); if(nextPosition.y < minY){ nextPosition.y = minY + maxGap.y; } else if(nextPosition.y > maxY){ nextPosition.y = maxY - maxGap.y; } } } Now let's make a prefab for the platforms, along with a material for it. You can do this by duplicating one of the skyline materials and changing its color to (255, 60, 255). Call it Platform Regular Mat. Then create a new cube, assign the materials to it, and turn it into a prefab called Platform. Put them both in the Platform folder. Also create a new empty object named Platform Manager and make it a child of Managers. Give it a Platform Manager component by draggin the script onto it. Make sure its position is (0, 0, 0) and assign our new Platform prefab to its Prefab field. Set its Number Of Objects to 6, its Recycle Offset to 20, its Min Size to (5, 1, 1), and its Max Size to (10, 1, 1). Then set the new fields Min Gap, Min Gap, Min Y, and Max Y to (2, -1.5, 0), (4, 1.5, 0), -5, and 10 respectively. Once again, you can experiment with different settings. Jumping and Falling Now that we have platforms, it's time to upgrade our runner. We'll use Unity's physics engine to make it jump, fall, and collide with the platforms, so add a Rigidbody component to Runner via Component / Physics / Rigidbody. We don't want it to rotate or disappear into the distance, so constrain it by freezing its Z position and all rotation axes. As movement will be accomplished by gliding across the platforms, let's create a physic material (Create / Physic Material) with no friction whatsoever. Set all its fields to zero and both combine options to maximum. This way friction will be determined by whatever it's gliding across. Name the new physic material Runner PMat, put it in the Runner folder, and assign it to the Material field of the Box Collider of Runner. Reposition Runner to (0, 2, 0) so that it will begin by falling down on the first platform. Then try out play mode to see what happens! So Runner falls on the flatform and then moves to the right. It even falls again after it moves past the platform. But when it happens to collide with the side of the next platform it behaves a bit weird. This is because we're still changing its position in an Update method. We should leave its movement to the physics engine and instead apply forces to it. Remove the call to Translate from the Update method of Runner. Instead, we'll use two of Unity's collision event methods – OnCollisionEnter and OnCollisionExit – to detect when we touch or leave a platform. As long as we're touching a platform, we apply an acceleration to make us run faster. Let's make the acceleration configurable and set it to 5 in the editor. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; private bool touchingPlatform; void Update () { distanceTraveled = transform.localPosition.x; } void FixedUpdate () { if(touchingPlatform){ rigidbody.AddForce(acceleration, 0f, 0f, ForceMode.Acceleration); } } void OnCollisionEnter () { touchingPlatform = true; } void OnCollisionExit () { touchingPlatform = false; } } One more thing we need to do before this works is assign a physic material to out platforms. Duplicate Runner PMat, rename it to Platform Regular PMat and move it to the Platform folder. Set both friction fields to 0.05 and drag the physic material onto the Platform prefab. Now our platforms provide a little friction, but Runner has a large enough acceleration pick up speed while moving across them. To make Runner jump, we need to detect the player's input. Unity's default settings for the jump action, found under Edit / Project Settings / Input, is to be triggered by pressing the space bar. We'll use that, and also configure 'x' as an alternative by putting it in the Alt Positive Button fied. We'll add a variable to Runner so we can configure its jump velocity. We'll use a vector instead of just a float so we can profide both a vertical and horizontal component. Set the corresponding field in the editor to (1, 7, 0). We want Runner to jump only when it's touching a platform while the jump button is pressed. Let's add code for this to the Update method. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 jumpVelocity; private bool touchingPlatform; void Update () { if(touchingPlatform && Input.GetButtonDown("Jump")){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); } distanceTraveled = transform.localPosition.x; } void FixedUpdate () { if(touchingPlatform){ rigidbody.AddForce(acceleration, 0f, 0f, ForceMode.Acceleration); } } void OnCollisionEnter () { touchingPlatform = true; } void OnCollisionExit () { touchingPlatform = false; } } Now we can jump! However, if Runner hits a flatform from the side, we can perform multiple jumps while it's still touching the platform, launching ourselves out of the gap. To prevent this, we'll decree that once jumped we are no longer touching the platform, even if we really are. This allows for one jump after colliding, which usually isn't enough to escape from a gap. (I've only included the Update method in the code below, nothing else changed.) void Update () { if(touchingPlatform && Input.GetButtonDown("Jump")){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); touchingPlatform = false; } distanceTraveled = transform.localPosition.x; } Platform Variety To spice things up, let's add two new platform types. One slows Runner down a bit, while the other speeds it up. We'll accomplish this by adding two physic materials, accompanied by two new colors, and pick which to use per platform at random. Duplicate Platform Regular PMat twice and name them Platform Slowdown PMat and Platform Speedup PMat. Also duplicate Platform Regular Mat twice and name them in a similar fashion. Set the friction values to 0.15 and 0, and their colors to (255, 255, 0) and (60, 130, 255), respectively. We now have to modify PlatformManager so it will assign these materials. We'll add two arrays for the materials and pick from them at random when recycling a platform. using UnityEngine; using System.Collections.Generic; public class PlatformManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 minSize, maxSize, minGap, maxGap; public float minY, maxY; public Material[] materials; public PhysicMaterial[] physicMaterials; private Vector3 nextPosition; private Queue objectQueue; void Start () { objectQueue = new Queue(numberOfObjects); for(int i = 0; i < numberOfObjects; i++){ objectQueue.Enqueue((Transform)Instantiate(prefab)); } nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } } void Update () { if(objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled){ Recycle(); } } private void Recycle () { Vector3 scale = new Vector3( Random.Range(minSize.x, maxSize.x), Random.Range(minSize.y, maxSize.y), Random.Range(minSize.z, maxSize.z)); Vector3 position = nextPosition; position.x += scale.x * 0.5f; position.y += scale.y * 0.5f; Transform o = objectQueue.Dequeue(); o.localScale = scale; o.localPosition = position; int materialIndex = Random.Range(0, materials.Length); o.renderer.material = materials[materialIndex]; o.collider.material = physicMaterials[materialIndex]; objectQueue.Enqueue(o); nextPosition += new Vector3( Random.Range(minGap.x, maxGap.x) + scale.x, Random.Range(minGap.y, maxGap.y), Random.Range(minGap.z, maxGap.z)); if(nextPosition.y < minY){ nextPosition.y = minY + maxGap.y; } else if(nextPosition.y > maxY){ nextPosition.y = maxY - maxGap.y; } } } Now it's a matter of assigning stuff to the arrays, either by dragging or by setting the array's size and using the dots. Make sure that both arrays are ordered the same way. Furthermore, if we'd like some platform types to be more common than others, simply include them multiple times. By including the regular materials twice and the others just once, the regular option has a 50% chance of occurring, while the others have a 25% chance each. Game Events Right now, the game starts as soon as we enter play mode and it doesn't end at all. What we want instead is that the game begins with a title screen and ends with a game over notice, where pressing one of the jump buttons starts a new game. For this approach we can identify three events that might require objects to take action. The first, game launch, is effectively handled by the Start methods. The other two, game start and game over, require a custom approach. We will create a very simple event manager class to handle them. Create a new folder named Managers and put a new C# script named GameEventManager in it. We make GameEventManager a static class that defines a GameEvent delegate type. Note that the manager isn't a MonoBehaviour and won't be attached to any Unity object. public static class GameEventManager { public delegate void GameEvent(); } Next, we use the new gameEvent type to add two events to our manager, GameStart and GameEnd. Now other scripts can subscribe to these events by assigning methods to them, which will be called when the events are triggered. public static class GameEventManager { public delegate void GameEvent(); public static event GameEvent GameStart, GameOver; } Finally, we need to include a way to trigger these events. We'll add two methods for this. Care should be taken to only call an event if anyone is subscribed to it, otherwise it will be null and the call will result in an error. public static class GameEventManager { public delegate void GameEvent(); public static event GameEvent GameStart, GameOver; public static void TriggerGameStart(){ if(GameStart != null){ GameStart(); } } public static void TriggerGameOver(){ if(GameOver != null){ GameOver(); } } } GUI and Game Start Now that we have a game start event, let's create a GUI and a manager that uses it. Let's add some text labels to our scene. To keep things organized, we'll use a container object to group them, so create a new empty game object with position (0, 0, 0) and name it GUI. Create three empty child objects for it and give each a GUIText component via Component / Rendering / GUIText. Set their Anchor fields to middle center so their text gets centered on their position. Name the first object Game Over Text, set its Text field to "GAME OVER", set its Font Size to 40, and set its Font Style to bold. Change its position to (0.5, 0.2, 0) so it ends up near the bottom center of the screen. Name the second object Instructions Text, also bold but with a font size of 20, and set its text to "press Jump (x or space) to play". Change its position to (0.5, 0.1, 0), right below the game over text. Name the third object Runner Text, with text "RUNNER", bold, and a font size of 60. It's position should be (0.5, 0.5, 0), right in the middle of the screen. Now create a C# script named GUIManager in the Managers folder and give it a GUIText variable for each text object we just made. Create a new object named GUI Manager and assign the script as a component. Make it a child of Managers. Then assign the text objects to the manager's corresponding fields. using UnityEngine; public class GUIManager : MonoBehaviour { public GUIText gameOverText, instructionsText, runnerText; } Now add a Start method to our new manager and use it to disable gameOverText so it won't be shown anymore. Also add an Update method that checks whether a jump button was pressed, and if so triggers the game-start event. using UnityEngine; public class GUIManager : MonoBehaviour { public GUIText gameOverText, instructionsText, runnerText; void Start () { gameOverText.enabled = false; } void Update () { if(Input.GetButtonDown("Jump")){ GameEventManager.TriggerGameStart(); } } } Now it's time to include a method to handle our game-start event, let's appropriately name it GameStart. We use this method to disable all text. We also disable the manager itself, so its Update method will no longer be called. If we didn't, each time we jump there'd be a new game-start event. using UnityEngine; public class GUIManager : MonoBehaviour { public GUIText gameOverText, instructionsText, runnerText; void Start () { gameOverText.enabled = false; } void Update () { if(Input.GetButtonDown("Jump")){ GameEventManager.TriggerGameStart(); } } private void GameStart () { gameOverText.enabled = false; instructionsText.enabled = false; runnerText.enabled = false; enabled = false; } } The last step is informing our event manager that it should call the GameStart method of our manager object, whenever the game-start event is triggered. We do this by adding our method to the event in the Start method. void Start () { GameEventManager.GameStart += GameStart; gameOverText.enabled = false; } Game Over Let's also add a handler for the game-over event to our gui manager. We go about the same way as for the game start event, but in this case we need to enable the manager again, along with the instructions and game over text. using UnityEngine; public class GUIManager : MonoBehaviour { public GUIText gameOverText, instructionsText, runnerText; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; gameOverText.enabled = false; } void Update () { if(Input.GetButtonDown("Jump")){ GameEventManager.TriggerGameStart(); } } private void GameStart () { gameOverText.enabled = false; instructionsText.enabled = false; runnerText.enabled = false; enabled = false; } private void GameOver () { gameOverText.enabled = true; instructionsText.enabled = true; enabled = true; } } The game over event should be triggered whenever Runner falls below the platforms. We'll simply add a Game Over Y field to it with a value of -6, then check each update whether we dropped below it. If so, we trigger the game over event. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 jumpVelocity; public float gameOverY; private bool touchingPlatform; void Update () { if(touchingPlatform && Input.GetButtonDown("Jump")){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); touchingPlatform = false; } distanceTraveled = transform.localPosition.x; if(transform.localPosition.y < gameOverY){ GameEventManager.TriggerGameOver(); } } void FixedUpdate () { if(touchingPlatform){ rigidbody.AddForce(acceleration, 0f, 0f, ForceMode.Acceleration); } } void OnCollisionEnter () { touchingPlatform = true; } void OnCollisionExit () { touchingPlatform = false; } } Using the Events Now that our game events are triggered correctly, it's time for Runner to take them into account. We want Runner to be hidden before the first game is started. We can do this by simply deactivating it in its Start method and activating it when the game start event is triggered. We'll also remember its starting position so we can reset it each game start. Let's reset distanceTraveled too, so it's immediately up to date. (I've omitted the contents of unmodified methods in the code below.) using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 jumpVelocity; public float gameOverY; private bool touchingPlatform; private Vector3 startPosition; void Start () { GameEventManager.GameStart += GameStart; startPosition = transform.localPosition; gameObject.active = false; } void Update () { … } void FixedUpdate () { … } void OnCollisionEnter () { … } void OnCollisionExit () { … } private void GameStart () { distanceTraveled = 0f; transform.localPosition = startPosition; gameObject.active = true; } } Additionally, let's freeze the position of Runner once it's game over. We can do this by making its rigidbody kinematic, which has the added benefit of resetting its velocity. Be sure to undo this change when a new game starts, though. Also, it's undesirable to keep the Update method running after the game ends, so we should disable the runner component until a new game begins. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 jumpVelocity; public float gameOverY; private bool touchingPlatform; private Vector3 startPosition; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; startPosition = transform.localPosition; gameObject.active = false; } void Update () { … } void FixedUpdate () { … } void OnCollisionEnter () { … } void OnCollisionExit () { … } private void GameStart () { distanceTraveled = 0f; transform.localPosition = startPosition; rigidbody.isKinematic = false; gameObject.active = true; enabled = true; } private void GameOver () { rigidbody.isKinematic = true; enabled = false; } } Now that Runner behaves, let's modify our platform manager so it reacts to the events in a similar fashion. It should only be enabled when a game is in progress and the platforms should only become visible after the first game start event has been triggered. We can achieve this by initially instantiating the platforms somewhere far behind the camera and moving the recyle loop to a new GameStart method. using UnityEngine; using System.Collections.Generic; public class PlatformManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 minSize, maxSize, minGap, maxGap; public float minY, maxY; public Material[] materials; public PhysicMaterial[] physicMaterials; private Vector3 nextPosition; private Queue objectQueue; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; objectQueue = new Queue(numberOfObjects); for(int i = 0; i < numberOfObjects; i++){ objectQueue.Enqueue((Transform)Instantiate (prefab, new Vector3(0f, 0f, -100f), Quaternion.identity)); } enabled = false; } void Update () { … } private void Recycle () { … } private void GameStart () { nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } enabled = true; } private void GameOver () { enabled = false; } } Now give SkylineManger the exact same treatment, so all parts of the game respond nicely to our events. using UnityEngine; using System.Collections.Generic; public class SkylineManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 minSize, maxSize; private Vector3 nextPosition; private Queue objectQueue; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; objectQueue = new Queue(numberOfObjects); for(int i = 0; i < numberOfObjects; i++){ objectQueue.Enqueue((Transform)Instantiate (prefab, new Vector3(0f, 0f, -100f), Quaternion.identity)); } enabled = false; } void Update () { … } private void Recycle () { … } private void GameStart () { nextPosition = transform.localPosition; for(int i = 0; i < numberOfObjects; i++){ Recycle(); } enabled = true; } private void GameOver () { enabled = false; } } Power-Up Let's include a power-up that allows for mid-air boosts. We'll make it a spinning cube that appears above platforms at random. We decide to have at most one such booster cube active in the scene at any moment, so we can suffice with one instance and reuse it. Create a new folder named Booster. In it, create a new material named Booster Mat. Because it's spinning, we'll use the Specular shader for the material, giving it a green (0, 255, 0) color and a white specular color. Now create a new cube, name is Booster, and set its scale to 0.5 to make it small. To make it a bit easier to hit, increase its collider's size to 1.5, which ends up being 0.75 due to the scale. Then assign its material to it. Mark the collider as a trigger, by checking its Is Trigger field. We do this because we want Runner to pass right through it, instead of colliding. Now create a new C# script named Booster in the corresponding folder and assign it to the Booster object. We start by giving it four public variables used to configure it. First, we need an offset from the platform's center to place the booster. Let's set it to (0, 2.5, 0). Second, we need a rotation velocity to make it spin. Let's use (45, 90, 1) to make it a bit lively. Third, we need a recycle offset, just as for platforms, in case Runner misses the power-up. Let's use a distance of 20. Fourth, we include a spawn chance to make the appearance of the booster somewhat unpredictable. A 25% chance per platform is fine. using UnityEngine; public class Booster : MonoBehaviour { public Vector3 offset, rotationVelocity; public float recycleOffset, spawnChance; } One way to make this work is by requesting a booster placement each time a platform is recycled. Then it's up to the booster itself whether it'll be placed. We'll add a method named SpawnIfAvailable to Booster for this. It requires a platform position so we know where to place the booster. using UnityEngine; public class Booster : MonoBehaviour { public Vector3 offset, rotationVelocity; public float recycleOffset, spawnChance; public void SpawnIfAvailable(Vector3 position){ } } We then add a variable to PlatformManager to which we assign Booster. Inside the Recycle method, we'll call its PlaceIfAvailable method after we've determined the new platform's position. using UnityEngine; using System.Collections.Generic; public class PlatformManager : MonoBehaviour { public Transform prefab; public int numberOfObjects; public float recycleOffset; public Vector3 minSize, maxSize, minGap, maxGap; public float minY, maxY; public Material[] materials; public PhysicMaterial[] physicMaterials; public Booster booster; private Vector3 nextPosition; private Queue objectQueue; void Start () { … } void Update () { … } private void Recycle () { Vector3 scale = new Vector3( Random.Range(minSize.x, maxSize.x), Random.Range(minSize.y, maxSize.y), Random.Range(minSize.z, maxSize.z)); Vector3 position = nextPosition; position.x += scale.x * 0.5f; position.y += scale.y * 0.5f; booster.SpawnIfAvailable(position); Transform o = objectQueue.Dequeue(); o.localScale = scale; o.localPosition = position; int materialIndex = Random.Range(0, materials.Length); o.renderer.material = materials[materialIndex]; o.collider.material = physicMaterials[materialIndex]; objectQueue.Enqueue(o); nextPosition += new Vector3( Random.Range(minGap.x, maxGap.x) + scale.x, Random.Range(minGap.y, maxGap.y), Random.Range(minGap.z, maxGap.z)); if(nextPosition.y < minY){ nextPosition.y = minY + maxGap.y; } else if(nextPosition.y > maxY){ nextPosition.y = maxY - maxGap.y; } } private void GameStart () { … } private void GameOver () { … } } Now we need to update the SpawnIfAvailable method so it activates and positions the booster, but only if it's not already active, and also taking the spawn chance into account. Also, to make this work the booster must begin deactivated and must also deactivate when the game ends. using UnityEngine; public class Booster : MonoBehaviour { public Vector3 offset, rotationVelocity; public float recycleOffset, spawnChance; void Start () { GameEventManager.GameOver += GameOver; gameObject.active = false; } public void SpawnIfAvailable (Vector3 position) { if(gameObject.active || spawnChance <= Random.Range(0f, 100f)) { return; } transform.localPosition = position + offset; gameObject.active = true; } private void GameOver () { gameObject.active = false; } } To make the booster spin and recycle, we have to add an Update method to it. Recycling is achieved by simple deactivation, as that makes it eligible for a respawn via SpawnIfAvailable. Rotation is achieved by rotating based on the elapsed time since the last frame. using UnityEngine; public class Booster : MonoBehaviour { public Vector3 offset, rotationVelocity; public float recycleOffset, spawnChance; void Start () { … } void Update () { if(transform.localPosition.x + recycleOffset < Runner.distanceTraveled){ gameObject.active = false; return; } transform.Rotate(rotationVelocity * Time.deltaTime); } public void SpawnIfAvailable (Vector3 position) { … } private void GameOver () { … } } At the moment Runner passed right through Booster, without anything happening. To change this, we add the Unity event method OnTriggerEnter to Booster, which is called whenever something hits its trigger collider. Because we know that the only thing that could possibly hit the booster is our runner, we can go ahead and give it a new booster power-up whenever there's a trigger. Let's assume Runner has a static method named AddBoost for this purpose, and use that. We also deactivate the booster, because it's been consumed. using UnityEngine; public class Booster : MonoBehaviour { public Vector3 offset, rotationVelocity; public float recycleOffset, spawnChance; void Start () { … } void Update () { … } void OnTriggerEnter () { Runner.AddBoost(); gameObject.active = false; } public void SpawnIfAvailable (Vector3 position) { … } private void GameOver () { … } } To make this work, we have to add an AddBoost method to Runner. To keep things simple, let's just add a private static variable to remember how many boosts we have accumulated. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 jumpVelocity; public float gameOverY; private bool touchingPlatform; private Vector3 startPosition; private static int boosts; void Start () { … } void Update () { … } void FixedUpdate () { … } void OnCollisionEnter () { … } void OnCollisionExit () { … } private void GameStart () { boosts = 0; distanceTraveled = 0f; transform.localPosition = startPosition; rigidbody.isKinematic = false; gameObject.active = true; enabled = true; } private void GameOver () { … } public static void AddBoost(){ boosts += 1; } } To actually allow mid-air jumps by consuming boosts, we need to modify the code that checks whether a jump is possible. Let's define a seperate boost velocity as well and set it to (10, 10, 0) for a nice boost. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 boostVelocity, jumpVelocity; public float gameOverY; private bool touchingPlatform; private Vector3 startPosition; private static int boosts; void Start () { … } void Update () { if(Input.GetButtonDown("Jump")){ if(touchingPlatform){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); touchingPlatform = false; } else if(boosts > 0){ rigidbody.AddForce(boostVelocity, ForceMode.VelocityChange); boosts -= 1; } } distanceTraveled = transform.localPosition.x; if(transform.localPosition.y < gameOverY){ GameEventManager.TriggerGameOver(); } } void FixedUpdate () { … } void OnCollisionEnter () { … } void OnCollisionExit () { … } private void GameStart () { … } private void GameOver () { … } public static void AddBoost(){ … } } Informative GUI It works! As long as we have boosts remaining, Runner can boost itself while in flight. It would be useful to actually see how many boost are available, so let's add a display for it to the GUI. While we're at it, let's show the distance traveled so far as well. Create a new object with a GUIText component as a child of GUI. Position it at (0.01, 0.99, 0), set its Anchor to upper left, give it font size 20 and a normal style. Name it Boosts Text. Create another such object, naming it Distance Text. Set its position to (0.5, 0.99, 0), with font size 30 and bold style. Its Anchor should be set to upper center. Add two variables to GUIManager for these new objects and assign them. using UnityEngine; public class GUIManager : MonoBehaviour { public GUIText boostsText, distanceText, gameOverText, instructionsText, runnerText; void Start () { … } void Update () { … } private void GameStart () { … } private void GameOver () { … } } Let's add two static methods to GUIManager which Runner can use to notify the GUI of changes to its distance traveled and boost count. Because the manager needs to use nonstatic variables in those methods, we add a static variable that references itself. That way the static code can get to the component instance which actually has the gui text elements. using UnityEngine; public class GUIManager : MonoBehaviour { public GUIText boostsText, distanceText, gameOverText, instructionsText, runnerText; private static GUIManager instance; void Start () { instance = this; GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; gameOverText.enabled = false; } void Update () { … } private void GameStart () { gameOverText.enabled = false; instructionsText.enabled = false; runnerText.enabled = false; enabled = false; } private void GameOver () { … } public static void SetBoosts(int boosts){ instance.boostsText.text = boosts.ToString(); } public static void SetDistance(float distance){ instance.distanceText.text = distance.ToString("f0"); } } Now all we need to do is let Runner call those methods whenever its distance or amount of boosts changes. using UnityEngine; public class Runner : MonoBehaviour { public static float distanceTraveled; public float acceleration; public Vector3 boostVelocity, jumpVelocity; public float gameOverY; private bool touchingPlatform; private Vector3 startPosition; private static int boosts; void Start () { … } void Update () { if(Input.GetButtonDown("Jump")){ if(touchingPlatform){ rigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange); touchingPlatform = false; } else if(boosts > 0){ rigidbody.AddForce(boostVelocity, ForceMode.VelocityChange); boosts -= 1; GUIManager.SetBoosts(boosts); } } distanceTraveled = transform.localPosition.x; GUIManager.SetDistance(distanceTraveled); if(transform.localPosition.y < gameOverY){ GameEventManager.TriggerGameOver(); } } void FixedUpdate () { … } void OnCollisionEnter () { … } void OnCollisionExit () { … } private void GameStart () { boosts = 0; GUIManager.SetBoosts(boosts); distanceTraveled = 0f; GUIManager.SetDistance(distanceTraveled); transform.localPosition = startPosition; rigidbody.isKinematic = false; gameObject.active = true; enabled = true; } private void GameOver () { … } public static void AddBoost(){ boosts += 1; GUIManager.SetBoosts(boosts); } } Particle Effects By now we have a functional game, but it feels a bit empty. Let's add some dust particles to fill the empty space and enhance the sense of depth and speed. Create a new a new particle system (GameObject / Create Other / Particle System) and make it a child of Runner with a position of (20, 0, 0) so it'll always stay to the right of the camera view. Set its Ellipsoid emitter to (10, 20, 20) so dust spawns in a large area. Set its Word Velocity to (-2, 0, 0) so the particles start with a velocity opposite to Runner and set Rnd Velocity to (2, 2, 2). The Emitter Velocity Scale should be 0 so movement of the emitter doesn't affect the initial velocity of the particles. To increase variety, set Min Size to 0.2, set Max Size to 0.8, increase Max Energy to 5, and decrease Min Emission to 10. Name the new object Dust Emitter. Next, create another particle system, also a child of Runner, and name it Trail Emitter. We'll use this one for a condensation trail effect left behind by Runner. We'll use a Mesh Particle Emitter this time, so remove its Ellipsoid Particle Emitter component and then add a new one via Component / Particles / Mesh Particle Emitter. Give it a Min Size of 0.2, a Max Size of 0.4, a Min Energy of 1, a Max Energy of 2, and set its Min Emission to 10. Select a cube for its Mesh field (by clicking on the dot). Make sure that the object's position is (0, 0, 0), so it overlaps with the runner's cube. As we only want to spawn particles when a game is in progress, we'll create a manager for them. Add a new C# script in the Managers folder and name it ParticleEmitterManager. Also create an appropriately named child object for Managers and assign the manager script to it. The only thing that ParticleEmitterManager has to do is switch the particle emitters on and off at the appropriate time. We'll use an array variable named emitters to hold references to all emitters that need to be managed. In this case, that's the two emitters we just created, but the manager can deal with any additional emitters you'd like to create. Assign our two particle emitters by dragging them to the Emitters field. using UnityEngine; public class ParticleEmitterManager : MonoBehaviour { public ParticleEmitter[] emitters; void Start () { GameEventManager.GameStart += GameStart; GameEventManager.GameOver += GameOver; GameOver(); } private void GameStart () { for(int i = 0; i < emitters.Length; i++){ emitters[i].ClearParticles(); emitters[i].emit = true; } } private void GameOver () { for(int i = 0; i < emitters.Length; i++){ emitters[i].emit = false; } } } That's it, we've finished the game! We can run and jump, leave a trail, collect power-ups, see our score, and have a scrolling background. It's a nice prototype that, with a lot of polish, you can transform into a finished game.
/
本文档为【创建固定视角滑动游戏】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索