Main » 2016 » May » 04 » Create simple game engine project
4:40 PM
Create simple game engine project
Create simple game engine project Create a new Android Studio project and call it Simple Game Engine. Create a blank Activity and call it SimpleGameEngine. Add the bob.png file (below) to the drawable folder as we did in the Drawing graphics demo. Here is a little bit of pseudo code so we can appreciate the structure of the real code we will write soon. The real code is quite long and at first glance might appear complex although it really isn’t. This pseudo code will clarify things a little and enable us to present and discuss the code in manageable chunks. You can also skip to the end of the article and see the code in its entirety for explicit clarification and context. You might like to simply copy and paste the full completed code into your project and get the simple game engine working; then come back and read all about it. Pseudo code not real code… SimpleGameEngine class{ onCreate method{ Initialize new GameView object Set new GameView object as the view }// End onCreate GameView class{ // An inner class within SimpleGameEngine Setup the class in special constructor method Run method calls update, draw and controls frame rate Update method draw method resume method:runs when game starts and sets up our thread pause method:runs when the game is shut down and stops our thread onTouchEvent method is called by Android when screen touched by player }// End of GameView inner class // Back in SimpleGameEngine class 2 more methods onResume method is called by Android when game starts and calls GameView resume onPause method is called by Android when game shuts down and calls GameView pause }// End of SimpleGameEngine and all of our code How it works Let’s run through how that works in sequence. Remember that although methods execute their code starting from the first line; the order in which methods are executed is determined by when they are called. So here is what will happen with our finished game engine. The player presses the icon on their device to play our game. Android does some stuff in the background that we don’t need to worry about; it then calls our onCreate method. In onCreate we declare and initialize a new class that we will code, called GameView. The last thing that onCreate does is to set our new GameView object as the view for the app. Once onCreate completes the Android operating system calls the onResume method (in the SimpleGameEngine class). This method calls the resume method, that we will write, in our new GameView class. Our resume method starts our thread which causes the run method to execute. The run method calls update and drawwhich essentially is the entire functionality of our game. The operating system will continually call run many times per second. So the GameView class handles our entire game, specifically, from the run method, until the user presses a button on their device (perhaps back or home) that causes the operating system to execute the onPause method in the SimpleGameEngine class which in turn calls the pause method in the GameView class and stops our thread. Therun method will not be called any more and our game will cease to function. In addition to the operating system calling our run method it will also call our onTouchEvent method (also in GameView) in the main/default thread. It is here we can then code a response to the players touches. Coding the Simple game engine class Here is the code for our SimpleGameEngine class with the extra methods (onPause and onResume) as well as our GameView class left out for now. Copy and paste this into your project. import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; public class SimpleGameEngine extends Activity { // gameView will be the view of the game // It will also hold the logic of the game // and respond to screen touches as well GameView gameView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Initialize gameView and set it as the view gameView = new GameView(this); setContentView(gameView); } // GameView class will go here // More SimpleGameEngine methods will go here } Above we add all the required imports we will need and a GameView object called gameView. Then we coded the onCreate method which simply initializes ourGameView object and sets it to the view of the entire game which is what the player will see on the screen. Coding the GameView class We will now code this all important GameView class and we will do so in chunks so we can talk about what is going on as we progress. We will add the simple extra methods (onResume and onPause) in the SimpleGameEngine class when we have completed GameView. If you think back to the Drawing Graphics demo, we drew everything to an object of type ImageView. And we set the object using setContentView as the view for the app. We can’t just set any old object to be the view for the app. Fortunately Android has some ready made classes that we can extend that provide us with everything we need. In the next block of code you see we declare our GameView class and use the extends keyword to give it all the functionality of an Android SurfaceView. This not only means we can easily draw graphics on it, like we did with ImageView but we can also use it in setContentView. Also in the very first line of code (excluding comments) we use the implements keyword followed by Runnable which means our class implements the Runnable interface that gives us all the thread functionality and allows us to use/override the run method that will become a kind of de-facto loop for our entire game. We also declare some objects and variables that we will need in the game. Enter the code below where the comment // GameView class will go here indicates. We will discuss the variables and objects afterwards. // Here is our implementation of GameView // It is an inner class. // Note how the final closing curly brace } // is inside SimpleGameEngine // Notice we implement runnable so we have // A thread and can override the run method. class GameView extends SurfaceView implements Runnable { // This is our thread Thread gameThread = null; // This is new. We need a SurfaceHolder // When we use Paint and Canvas in a thread // We will see it in action in the draw method soon. SurfaceHolder ourHolder; // A boolean which we will set and unset // when the game is running- or not. volatile boolean playing; // A Canvas and a Paint object Canvas canvas; Paint paint; // This variable tracks the game frame rate long fps; // This is used to help calculate the fps private long timeThisFrame; // Declare an object of type Bitmap Bitmap bitmapBob; // Bob starts off not moving boolean isMoving = false; // He can walk at 150 pixels per second float walkSpeedPerSecond = 150; // He starts 10 pixels from the left float bobXPosition = 10; In the code above, after the class declaration that we have already discussed, we declare and initialize a Thread object called gameThread. We initialize it to null which is kind of like zero or no value as we are not ready to use it yet. After that we declare an object of type SurfaceHolder called surfaceHolder. This is new to us and it is an extra requirement when drawing graphics in a thread. As the operating system might like to draw to the screen as well, perhaps a text notification etc, we need to lock the drawing surface each time the run method calls our draw method. We will see this in action soon. We then declare a boolean called playing. You might have noticed the odd looking volatile keyword we used as well. The variable will be used to determine if the game is playing and we can then wrap the calls to draw and update in a while(playing) loop. We use volatile because it is a variable that can be used from inside and outside the thread, as we will see. We then decalre two variables fps and timeThisFrame. We will see how we use them to measure the frames per second of our game and use this measurement to make Bob move at the same speed on different devices with different hardware and therefore different frame rates. This consistency is essential for any game we code because a game shouldn’t give a different experience between devices. Next we declare a Bitmap, bitmapBob to hold our image of Bob. Following this we have a boolean, isMoving. We will use this to decide when to move Bob and when to leave him stationary. We then declare and initialize walkSpeedPerSecond which will determine the speed in pixels per second that bob can walk. Finally bobXPositionwill be used when we call canvas.drawBitmap to position Bob on the x axis; so we can manipulate it and Bob will glide across the screen as intended. In the next block of code we have a curiously named method, GameView. Even more curious is it has no return type? This is a special method called a constructor. We do not have to code a constructor unless we want to, but here it is really useful. When we initialize our gameView object, that is, when we call gameView = new GameView(), this constructor method is called. It allows us do do all sorts of initialization ready for when we use our class for its main intended purpose. Also in the method signature we can see the this object is passed in. We need this to initialize our bitmap and call the constructor of the SurfaceView class. In our constructor method we see that we first call the SurfaceView constructor using the super keyword, then initialize ourHolder, paint and bitmapBob objects. Enter the code below that we have just discussed and we will take a look at our run method. // When the we initialize (call new()) on gameView // This special constructor method runs public GameView(Context context) { // The next line of code asks the // SurfaceView class to set up our object. // How kind. super(context); // Initialize ourHolder and paint objects ourHolder = getHolder(); paint = new Paint(); // Load Bob from his .png file bitmapBob = BitmapFactory.decodeResource(this.getResources(), R.drawable.bob); } Now we come to the centre of the universe as far as our simple game engine is concerned. The run method. Remember that the operating system continuously calls this method from when we start our thread right round to when we stop it. So we can think of the run method as an infinite loop that will execute until the game is shut down. This is sometimes referred to as the game loop or game main. In it we wrap our code in a while (playing) loop because the thread can feasibly start before we are fully setup. The first thing we do if playing is true is get the system time in thousandths of a second and store it in the startFrameTime variable. Then we call update and draw. These two methods will hold the logic and graphics code of our game. After this we get the current system time again, subtract startFrameTime from it and store the result in timeThisFrame. This is the number of milliseconds it took to execute our update and draw methods this frame. The if statement that follows makes sure the answer is not zero and if it isn’t we assign the result of 1000 divided by timeThisFrame to fps. This is our current frames per second. We will see how we use that really soon. Enter the run method below and we can move on to discussing the update method. @Override public void run() { while (playing) { // Capture the current time in milliseconds in startFrameTime long startFrameTime = System.currentTimeMillis(); // Update the frame update(); // Draw the frame draw(); // Calculate the fps this frame // We can then use the result to // time animations and more. timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame > 0) { fps = 1000 / timeThisFrame; } } } Here is update below. All we do is check if isMoving is true and if it is we increase Bob’s x position (bobXPosition) by walkSpeedPerSecond divided by fps. This will achieve the same movement speed for Bob regardless of the current frames per second (fps) the device is running at. That’s it for update. // Everything that needs to be updated goes in here // In later projects we will have dozens (arrays) of objects. // We will also do other things like collision detection. public void update() { // If bob is moving (the player is touching the screen) // then move him to the right based on his target speed and the current fps. if(isMoving){ bobXPosition = bobXPosition + (walkSpeedPerSecond / fps); } } Now we will draw Bob and some extra text to display the current frames per second. We have seen all the drawing code before in the Drawing graphics demo but there are some extra lines of code and one subtle more difference as well. First we attempt to get a lock on the drawing surface and if successful we make the drawing surface our canvas. If you look closely to the call to canvas.drawBitmap, instead of providing a number for the x coordinate we use bobXPosition. We have already seen in the update method how we manipulate this variable. The only thing we haven’t seen but we will very soon is how we toggle between isMoving being true or false. Lastly we draw everything to the screen, unlock the drawing surface and hand control back to the operating system- for a few milliseconds anyway. Add the draw method to your project. // Draw the newly updated scene public void draw() { // Make sure our drawing surface is valid or we crash if (ourHolder.getSurface().isValid()) { // Lock the canvas ready to draw // Make the drawing surface our canvas object canvas = ourHolder.lockCanvas(); // Draw the background color canvas.drawColor(Color.argb(255, 26, 128, 182)); // Choose the brush color for drawing paint.setColor(Color.argb(255, 249, 129, 0)); // Make the text a bit bigger paint.setTextSize(45); // Display the current fps on the screen canvas.drawText("FPS:" + fps, 20, 40, paint); // Draw bob at bobXPosition, 200 pixels canvas.drawBitmap(bitmapBob, bobXPosition, 200, paint); // Draw everything to the screen // and unlock the drawing surface ourHolder.unlockCanvasAndPost(canvas); } } Now we have our two methods which are called by the methods of the similar name in SimpleGameEngine. Remember that the near namesakes in SimpleGameEngineare called by the operating system when our game is started and stopped. The pause method looks more complicated than it really is. The unusual looking try and catch blocks are a requirement forced upon us because of the design of theThread class. We don’t need to completely understand them in order to make games. The try block is the code to execute and the catch block is what to do if the tryblock fails. Not unlike if and else. The important code for the pause method is when we call gameThread.join() and set playing to false this will shut down our game thread. The resume method sets playing to true, initializes our thread then starts it. The game is now running. // If SimpleGameEngine Activity is paused/stopped // shutdown our thread. public void pause() { playing = false; try { gameThread.join(); } catch (InterruptedException e) { Log.e("Error:", "joining thread"); } } // If SimpleGameEngine Activity is started theb // start our thread. public void resume() { playing = true; gameThread = new Thread(this); gameThread.start(); } Getting player input One extra benefit of the SurfaceView class which we extended to make GameView is that it allows us to override the onTouchEvent method. The onTouchEventmethod is called directly by the operating system every time the screen is touched. Within this method we can use the data passed in, in the MotionEvent object to determine if the player is telling our game to do something. This method will get more complicated depending upon the control system you are trying to implement. Future game projects will go into this method in much more detail. For now all we need to do is switch as shown in the code below and handle two cases. ACTION_DOWN when the player touches the screen and ACTION_UP when they remove their finger from the screen. All we do is set isMoving to either true or false for each of those cases respectively and we have implemented our game controls. // The SurfaceView class implements onTouchListener // So we can override this method and detect screen touches. @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { // Player has touched the screen case MotionEvent.ACTION_DOWN: // Set isMoving so Bob is moved in the update method isMoving = true; break; // Player has removed finger from screen case MotionEvent.ACTION_UP: // Set isMoving so Bob does not move isMoving = false; break; } return true; } Now we are at the end of our GameView inner class so add the closing curly brace }. } // This is the end of our GameView inner class Finishing off the main class Now we are back in the SimpleGameEngine class where we add the overridden methods onResume and onPause which will be called by the operating system when the game is started or stopped. All they do is call the near namesake methods in the GameView class to control our thread- and by default our entire game. Enter the last block of code for our simple game engine. Be sure to do so within the final closing curly brace } of the SimpleGameEngine class. // This method executes when the player starts the game @Override protected void onResume() { super.onResume(); // Tell the gameView resume method to execute gameView.resume(); } // This method executes when the player quits the game @Override protected void onPause() { super.onPause(); // Tell the gameView pause method to execute gameView.pause(); } Running the simple game engine You can now run the game on a device and see Bob glide smoothly across the screen when it is pressed. Albeit backwards :-D. Although that was a slightly more lengthy project than some of the previous ones; we have achieved even more than it might seem. We now have a framework/engine which we can reuse again and again. If every single line of code is not crystal clear it don’t worry because as we expand and improve the code we will become more familiar with what is going on. As each of the future projects gets progressively more advanced we will also introduce more Java and Android concepts. Let’s move on and make a full, playable game. Complete code listing package com.gamecodeschool.simplegameengine; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; public class SimpleGameEngine extends Activity { // gameView will be the view of the game // It will also hold the logic of the game // and respond to screen touches as well GameView gameView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Initialize gameView and set it as the view gameView = new GameView(this); setContentView(gameView); } // GameView class will go here // Here is our implementation of GameView // It is an inner class. // Note how the final closing curly brace } // is inside SimpleGameEngine // Notice we implement runnable so we have // A thread and can override the run method. class GameView extends SurfaceView implements Runnable { // This is our thread Thread gameThread = null; // This is new. We need a SurfaceHolder // When we use Paint and Canvas in a thread // We will see it in action in the draw method soon. SurfaceHolder ourHolder; // A boolean which we will set and unset // when the game is running- or not. volatile boolean playing; // A Canvas and a Paint object Canvas canvas; Paint paint; // This variable tracks the game frame rate long fps; // This is used to help calculate the fps private long timeThisFrame; // Declare an object of type Bitmap Bitmap bitmapBob; // Bob starts off not moving boolean isMoving = false; // He can walk at 150 pixels per second float walkSpeedPerSecond = 150; // He starts 10 pixels from the left float bobXPosition = 10; // When the we initialize (call new()) on gameView // This special constructor method runs public GameView(Context context) { // The next line of code asks the // SurfaceView class to set up our object. // How kind. super(context); // Initialize ourHolder and paint objects ourHolder = getHolder(); paint = new Paint(); // Load Bob from his .png file bitmapBob = BitmapFactory.decodeResource(this.getResources(), R.drawable.bob); // Set our boolean to true - game on! playing = true; } @Override public void run() { while (playing) { // Capture the current time in milliseconds in startFrameTime long startFrameTime = System.currentTimeMillis(); // Update the frame update(); // Draw the frame draw(); // Calculate the fps this frame // We can then use the result to // time animations and more. timeThisFrame = System.currentTimeMillis() - startFrameTime; if (timeThisFrame > 0) { fps = 1000 / timeThisFrame; } } } // Everything that needs to be updated goes in here // In later projects we will have dozens (arrays) of objects. // We will also do other things like collision detection. public void update() { // If bob is moving (the player is touching the screen) // then move him to the right based on his target speed and the current fps. if(isMoving){ bobXPosition = bobXPosition + (walkSpeedPerSecond / fps); } } // Draw the newly updated scene public void draw() { // Make sure our drawing surface is valid or we crash if (ourHolder.getSurface().isValid()) { // Lock the canvas ready to draw canvas = ourHolder.lockCanvas(); // Draw the background color canvas.drawColor(Color.argb(255, 26, 128, 182)); // Choose the brush color for drawing paint.setColor(Color.argb(255, 249, 129, 0)); // Make the text a bit bigger paint.setTextSize(45); // Display the current fps on the screen canvas.drawText("FPS:" + fps, 20, 40, paint); // Draw bob at bobXPosition, 200 pixels canvas.drawBitmap(bitmapBob, bobXPosition, 200, paint); // Draw everything to the screen ourHolder.unlockCanvasAndPost(canvas); } } // If SimpleGameEngine Activity is paused/stopped // shutdown our thread. public void pause() { playing = false; try { gameThread.join(); } catch (InterruptedException e) { Log.e("Error:", "joining thread"); } } // If SimpleGameEngine Activity is started then // start our thread. public void resume() { playing = true; gameThread = new Thread(this); gameThread.start(); } // The SurfaceView class implements onTouchListener // So we can override this method and detect screen touches. @Override public boolean onTouchEvent(MotionEvent motionEvent) { switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { // Player has touched the screen case MotionEvent.ACTION_DOWN: // Set isMoving so Bob is moved in the update method isMoving = true; break; // Player has removed finger from screen case MotionEvent.ACTION_UP: // Set isMoving so Bob does not move isMoving = false; break; } return true; } } // This is the end of our GameView inner class // More SimpleGameEngine methods will go here // This method executes when the player starts the game @Override protected void onResume() { super.onResume(); // Tell the gameView resume method to execute gameView.resume(); } // This method executes when the player quits the game @Override protected void onPause() { super.onPause(); // Tell the gameView pause method to execute gameView.pause(); } } using UnityEngine; using System.Collections; namespace Completed { //Player inherits from MovingObject, our base class for objects that can move, Enemy also inherits from this. public class Player : MovingObject { public float restartLevelDelay = 1f; //Delay time in seconds to restart level. public int pointsPerFood = 10; //Number of points to add to player food points when picking up a food object. public int pointsPerSoda = 20; //Number of points to add to player food points when picking up a soda object. public int wallDamage = 1; //How much damage a player does to a wall when chopping it. private Animator animator; //Used to store a reference to the Player's animator component. private int food; //Used to store player food points total during level. //Start overrides the Start function of MovingObject protected override void Start () { //Get a component reference to the Player's animator component animator = GetComponent(); //Get the current food point total stored in GameManager.instance between levels. food = GameManager.instance.playerFoodPoints; //Call the Start function of the MovingObject base class. base.Start (); } //This function is called when the behaviour becomes disabled or inactive. private void OnDisable () { //When Player object is disabled, store the current local food total in the GameManager so it can be re-loaded in next level. GameManager.instance.playerFoodPoints = food; } private void Update () { //If it's not the player's turn, exit the function. if(!GameManager.instance.playersTurn) return; int horizontal = 0; //Used to store the horizontal move direction. int vertical = 0; //Used to store the vertical move direction. //Get input from the input manager, round it to an integer and store in horizontal to set x axis move direction horizontal = (int) (Input.GetAxisRaw ("Horizontal")); //Get input from the input manager, round it to an integer and store in vertical to set y axis move direction vertical = (int) (Input.GetAxisRaw ("Vertical")); //Check if moving horizontally, if so set vertical to zero. if(horizontal != 0) { vertical = 0; } //Check if we have a non-zero value for horizontal or vertical if(horizontal != 0 || vertical != 0) { //Call AttemptMove passing in the generic parameter Wall, since that is what Player may interact with if they encounter one (by attacking it) //Pass in horizontal and vertical as parameters to specify the direction to move Player in. AttemptMove (horizontal, vertical); } } //AttemptMove overrides the AttemptMove function in the base class MovingObject //AttemptMove takes a generic parameter T which for Player will be of the type Wall, it also takes integers for x and y direction to move in. protected override void AttemptMove (int xDir, int yDir) { //Every time player moves, subtract from food points total. food--; //Call the AttemptMove method of the base class, passing in the component T (in this case Wall) and x and y direction to move. base.AttemptMove (xDir, yDir); //Hit allows us to reference the result of the Linecast done in Move. RaycastHit2D hit; //If Move returns true, meaning Player was able to move into an empty space. if (Move (xDir, yDir, out hit)) { //Call RandomizeSfx of SoundManager to play the move sound, passing in two audio clips to choose from. } //Since the player has moved and lost food points, check if the game has ended. CheckIfGameOver (); //Set the playersTurn boolean of GameManager to false now that players turn is over. GameManager.instance.playersTurn = false; } //OnCantMove overrides the abstract function OnCantMove in MovingObject. //It takes a generic parameter T which in the case of Player is a Wall which the player can attack and destroy. protected override void OnCantMove (T component) { //Set hitWall to equal the component passed in as a parameter. Wall hitWall = component as Wall; //Call the DamageWall function of the Wall we are hitting. hitWall.DamageWall (wallDamage); //Set the attack trigger of the player's animation controller in order to play the player's attack animation. animator.SetTrigger ("playerChop"); } //OnTriggerEnter2D is sent when another object enters a trigger collider attached to this object (2D physics only). private void OnTriggerEnter2D (Collider2D other) { //Check if the tag of the trigger collided with is Exit. if(other.tag == "Exit") { //Invoke the Restart function to start the next level with a delay of restartLevelDelay (default 1 second). Invoke ("Restart", restartLevelDelay); //Disable the player object since level is over. enabled = false; } //Check if the tag of the trigger collided with is Food. else if(other.tag == "Food") { //Add pointsPerFood to the players current food total. food += pointsPerFood; //Disable the food object the player collided with. other.gameObject.SetActive (false); } //Check if the tag of the trigger collided with is Soda. else if(other.tag == "Soda") { //Add pointsPerSoda to players food points total food += pointsPerSoda; //Disable the soda object the player collided with. other.gameObject.SetActive (false); } } //Restart reloads the scene when called. private void Restart () { //Load the last scene loaded, in this case Main, the only scene in the game. Application.LoadLevel (Application.loadedLevel); } //LoseFood is called when an enemy attacks the player. //It takes a parameter loss which specifies how many points to lose. public void LoseFood (int loss) { //Set the trigger for the player animator to transition to the playerHit animation. animator.SetTrigger ("playerHit"); //Subtract lost food points from the players total. food -= loss; //Check to see if game has ended. CheckIfGameOver (); } //CheckIfGameOver checks if the player is out of food points and if so, ends the game. private void CheckIfGameOver () { //Check if food point total is less than or equal to zero. if (food <= 0) { //Call the GameOver function of GameManager. GameManager.instance.GameOver (); } } } }
Views: 298 | Added by: nicvic | Rating: 0.0/0
Total comments: 0
avatar