CSE1OOF Assignment Part C
{`Department of Computer Science and Information Technology La Trobe University CSE1OOF Semester 1 Bundoora, 2020 Assignment Part C `}
Problem Description
Your task will be to make a 2D game that works inside the UNIX terminal window.
Please watch the Week 9 & 10 Lecture Videos on Echo.
The game will use only Unicode characters (basically Emojis) to represent the player and the other entities, such as monsters etc.
The player will be able to move around using the WASD keys.
The artificial intelligence (AI) of the monsters will be synchronous with the player movements, that is, the monsters only move when the player moves. This is because taking user input is a blocking operation meaning that the code will sit and wait for the user to provide input, during this time no processing can happen.
I have provided you with some files copy them to your home drive using this command.
cp /home/student/csilib/cse1oof/bu1-2020/game/*.java .
Your program must work with an UNEDITED version of GameDriver.java
The driver program will put the terminal into a special mode where they key pressed are processed immediately without needing to press enter. THIS UNIX SPECIFIC! USE LATCS8
It won’t work on Windows. If you don’t want to use Latcs8, I would recommend using the Virtual Machine (VM) that is on LMS. Windows Subsystem for Linux may also work. https://lms.latrobe.edu.au/mod/forum/discuss.php?d=962672
We will be testing the program on Latcs8, it MUST work correctly there.
Inheritance Diagram
Terminology
In the game making word there is a lot of jargon that you need to be aware of. Game State
The state of a game refers to overall position of the player and any other entities and their attributes, position, health, etc.
For a game of chess, the game state information would include
- Who’s turn is it
- What colour each player is playing
- What the location of each piece is. Render
The term render in computing generally refers to the idea of converting raw data into an image for a human to look at. This can include thigs like video rendering, or image rendering. For our game rendering will refer to printing the entities on the terminal screen.
Artificial intelligence (AI)
This refers to how the game entities behave; where they move, what tasks they do and when they do them. The AI can be very simple such as move in a random direction, or the AI can be very complex such as a system that predicts the user’s behaviour.
Map
The environment where the game takes place with the entities on itis referred to as the Map or Level. Not to be confused with the concept of player level, which is typically independent of the map. In our game the map is a finite two-dimensional grid.
Mob
A mob is a general term for hostile enemies or monsters. Sprite
A sprite is the image that is used to represent a game entity, in our case we will be using Emojis as sprites. A good source of spites: https://www.compart.com/en/unicode/block/U+1F300
Frame
A frame is the rendered image that the user sees, the frame rate refers to how many rendered images are generated and displayed to the user’s screen per second. Most games will have a frame rate between 60-120 FPS, depending of graphics settings and how powerful the PC is.
Our game will only refresh the frame when the user moves Tick
A tick is 1 cycle of the game loop which updates the entities in the game. Most games will have a timer-based tick system, for example Minecraft has a tick rate of 20 ticks per second (TPS).
Our game will only tick when the player moves.
Game Design
The game we are designing draws inspiration from Chips Challenge, I would recommend familiarising yourself with the game before continuing. Chips Challenge uses a moving frame to allow the player to see only a small section of the map at a time. This allows the map to be much larger than the users screen.
To keep this assignment simple, we will not be doing that, instead will have a fixed size map that is the same size as the frame. However, we will explore how to do a moving window type approach in the bonus section.
The coordinates system will be positive X moves to the right and positive Y moves down. The squares below we will refer to as tiles, or positions.
The player has no health, instead the player will lose immediately as soon as a monster moves onto a tile that is occupied by the player. The player can move by pressing WASD the keys. The monsters move 1 tile per tick. The player wins the game by reaching the flag.
We will have a few monsters, if the player touches any monster the player loses instantly.
- The Ghost, moves around
- The Cactus, does not
- The Ball, travels in one direction until it cannot go any further, then it bounces
- The Alien, moves in leftwards, until it cannot go any further, then it will turn 90 degrees to the left and
- The Goblin, moves towards the player. And some other entities
- The Player
- The Wall, stops all entities passing it, does not
- The MultiWall, same as Wall, but occupies multiple
- The SafeZone, same as MultiWall, but allows players to walk through
- The Flag, wins the game, does not move
Our game will have a collision system, so some entities can walk though others. Monsters must collide with other monsters, but not with the player.
GameState.java
This will be your main controller object for the game, it will contain the player and all entities.
|
In order to be compatible with the drive program you MUST implement the methods listed below.
The underlined attributes are static constants
Constructor
The constructor will need to
- Initialise the frame array to be FRAME_WIDTH by
- Initialise the Random
- Call resetGame()
resetGame()
This method will setup the game into a fresh state.
- Initialise the Entities
- Populate the entities array with some monsters and a
- Instantiate the player
- Set gameFinished to false
Do not put the player in the entities array.
onUserInput(int asciiCode)
This method will be called when the user presses a key, the ASCII code will be passed in.
Your code should detect if the pressed “W” and if so, call moveUp() on the player object.
Do the same for left, right, and down with keys A, D, and S respectively.
If the user pressed “R”, call resetGame() to start a new game.
renderFrame()
This method must do a few tasks
- Clear the contents of the frame array, use a nested for loop to set everything to
- Loop over the entities array and call drawSelf().
- Call drawSelf() for the player
- Display the contents of frame, use a nested for loop to print the strings out. If you encounter a null, just print two space
When printing the frame put a nice border on it. Use the same box drawing characters that were used in Assignment 2.
getRandom()
Return the Random object.
isBlocked(Entity entityToMove, int x, int y)
This method is called when an entity wants to move to a new location, we will loop over the entities in the array and for each entity in the entity array, call isBlockingTo() and occupiesPosition() if both are true that means that this location is blocked, so return true.
isOutOfBounds(int x, int y)
Return true if ether the X or Y value is outside of the map.
Valid range for the X coordinate is 0-19. (because the map is 20 wide) Valid range for the Y coordinate is 0-9. (because the map is 10 high)
Don’t hard code the numbers, use the constants FRAME_WIDTH and FRAME_HEIGHT
drawSpriteAt(int x, int y, String sprite)
Put the sprite string into the frame array at index x,y. If the coordinates are out of bounds, then do nothing.
tick()
This method will be called by the driver to update the game state, this method needs to:
- Check if gameFinished is true, if so, return immediately and do
- Loop over all entities in the array and call tick() on the
- Check if gameFinished is true, if so show them a message telling them they can press “Esc” to quit, or press “Enter” to try
stop()
This method is called when the player has reached the goal, or when they died. It must call renderFrame(), then set the gameFinished attribute to true.
checkPlayerColision()
loop over all entities in the array and see if the entity occupies (use occupiesPosition()) the same X&Y position as the player, if so call onPlayerColision() for that entity.
removeEntity(Entity deleteMe)
loop over the entities array and if you find an entity that equals deleteMe set that index to null.
Entity.java
This will be the base class that all the other entities will extend, it will hold common code that is shared between all entities.
Entity |
- GameState gameState - int x - int y |
+ Entity(GameState gs, int x, int y) + boolean canMoveTo(int x, int y) + int getX() + int getY() + void moveUp() + void moveDown() + void moveRight() + void moveLeft() + boolean canMoveUp() + boolean canMoveDown() + boolean canMoveRight() + boolean canMoveLeft() + GameState getGameState() + void drawSprite() + void tick() + boolean occupiesPosition(int x, int y) + boolean isBlockingTo(Entity e) + void onPlayerColision(Player p) |
The attributes will be initialised via the constructor. The game state is passed in so that we can get the player and other data from the game state. Set isDeleted to false.
The Getter Methods (getX(), getY(), etc)
The getters are easy, just return the relevant attribute.
drawSprite()
This method must draw its sprite to the GameState object by calling drawSpriteAt() and passing in the X, Y and a sprite string (an Emoji), for this class just put a placeholder sprite of your choice, we will override this method in the subclasses anyway.
tick()
We will override this in the subclass, so leave the method empty.
canMoveTo(int newx, iny newy)
This method is used to see if this entity considers the given position to be a valid location to move to. Return true if the position is valid, false otherwise.
A valid position must not be out of bounds and must not be blocked. Use GameState.isOutOfBounds() and GameState.isBlocked() to check.
The Directional Can Move Methods (Up,Down,Left,Right)
These will simply call canMoveTo() with the relevant parameters and return its result. Increasing x moves to the right, increasing y moves down.
For example, if we wanted to know if we can move to the right, we would call
canMoveTo(x+1,y)
The Directional Move Methods (Up,Down,Left,Right)
Call the relevant canMove method, if it returns true then increment or decrement the X or Y valuable accordingly, then call checkPlayerColision().
occupiesPosition(int x, int y)
Return true if the entity exists at this location.
Basically, just check if parameter X equals the attribute X and parameter Y equals attribute Y
isBlockingTo(Entity other)
The entity other wants to move onto this entities tile, should it be allowed to do so? Return false for now, we will override it later.
onPlayerColision(Player p)
This will be called when the player moves into the tile that this entity occupies. Leave this method empty, we will override it later.
Player.java
Player extends Entity |
+ Player(GameState gs, int x, int y) + void drawSelf() |
The player class is easy because most of its behaver is inherited from Entity.
You just need to override the drawSelf() method. This method needs to call GameState.drawSpriteAt() with an appropriate sprite Emoji.
Make sure to choose a sprite that is clearly a player and not going to be confused with a monster. Something like , or would be a good choice.
Flag.java
This will be the goal of the game.
Flag extends Entity |
+ Player(GameState gs, int x, int y) + void drawSelf() + onPlayerColision(Player p) |
When the player collides with this entity, call GameSate.stop() and print a message.
Monster.java
This will be a super class to all monsters so we can put all common monster code in one place.
Monster extends Entity |
+ Monster(GameState gs, int x, int y) + isBlockingTo(Entity other) |
isBlockingTo(Entity other)
If the other entity is a player, return true, otherwise return false. This will make the monsters solid to all entities except the player.
Cactus.java
This is a “monster” that is stationary, it will not move around
Cactus extends Monster |
+ Cactus(GameState gs, int x, int y) + void onPlayerColision(Player p) + void drawSelf() |
Override the player collision method and call GameState.stop() and print a message to the user. drawSelf() should call GameState.drawSpriteAt() with an appropriate sprite Emoji.
Ghost.java
The Ghost will move around the map in random directions
Ghost extends Monster |
+ Ghost(GameState gs, int x, int y) + void tick() + void onPlayerColision(Player p) + void drawSelf() |
The Ghost will move around the map in random directions
Inside the tick method, get the random object from GameState and use it to determine what direction to move in, if any.
For example you could use nextInt(5) top get a random int between 0 and 4.
If the value equals 1, call the moveUp() method, if equals 2 call moveDown(), etc.
Wall.java
To make the map more interesting we need to have some sort of barrier.
Wall extends Entity |
- int width - int height |
+ Wall(GameState gs, int x, int y) + void drawSelf() + boolean isBlockingTo(Entity other) |
isBlockingTo(Entity other)
just return true, obviously a wall is always solid.
drawSelf()
I would recommend the FULL BLOCK Unicode character █
Remember that the emojis we are using two characters wide, so you will need to use two FULL BLOCKs to keep everything aligned.
MultiWall.java
For large walls its inefficient to have hundreds of individual Wall entities. So instead we will allow a single wall entity to occupy multiple tiles at once. The size of the wall will be provided by the constructor.
MultiWall extends Wall |
- int width - int height |
+ MultiWall(GameState gs, int x, int y, int with, int height) + boolean occupiesPosition(int x, int y) + void drawSelf() + int getHeight() + int getWidth() |
We need to override occupiesPosition because the wall will occupy multiple tiles.
So, for example, if we created a wall at position (3,4) with a size of 2x2 then it would occupy four positions, (3,4), (3,5), (4,4), (4,5) see the image to the right.
You can do some simple math to calculate if a given location is inside the wall or not.
drawSelf()
This needs to use two for() loops to draw the █ characters.
SafeZone.java
Provide a safe zone for the player, basically a “Wall” that players can walk through, but monsters cannot. This will give the player some refuge from the Goblin.
SafeZone extends MultiWall |
+ SafeZone(GameState gs, int x, int y, int with, int height) + boolean occupiesPosition(int x, int y) + boolean isBlockingTo(Entity other) + void drawSelf() |
drawSelf()
This will be exactly the same as it was in MultiWall, but with a different sprite.
Use a sprite that is less solid looking. I would recommend the LIGHT SHADE ░ character, it looks like gravel.
isBlockingTo(Entity other)
Return false if the “other” object is a player, return true for everything else.
Goblin.java
The Goblin will move towards the player.
Goblin extends Monster |
+ Goblin(GameState gs, int x, int y) + void tick() + void drawSelf() |
tick()
The way to approach this task is to subtract the Goblins position from the Players position.
If the player was as (1,3) and the Goblin was at (5,1) then the difference would be (-4,2) store these in two variables, xDiff and yDiff.
Of the two diff values choose the one the one with the highest absolute value. In this case the X coordinate is highest, so the Goblin should try and move in the
negative X axis.
If xDiff > 0, then you want to see if you can move to the left if you can, do so, otherwise there must be a wall or something in the way, so look at yDiff and if its bigger than zero, move up, if its smaller than zero move down.
If xDiff is < 0, then you want to see if you can move to the right if you can, do so, otherwise there must be a wall or something in the way, so look at yDiff and if its bigger than zero, move up,
if its smaller than zero move down. The same logic applies for yDiff.
The Goblin is formidable, if you want to nurf it because it’s too strong, what you could do is have a counter attribute inside the Goblin so that every 3rd tick it stays still. Which will slow it down.
Ball.java & Alien.java
You should be able to figure out what goes in these files.
Task 1
You MUST read this entire document and watch the Lecture videos for Week 9 and 10. Seriously the videos show how to do most of the assignment.
Task 2
Do task 1 again.
Task 3
Copy the files from the CSILIB directory to your home directory.
Task 4
Get the GameState.renderFrame() method working, obviously there are no entities yet so just make sure it has a nice border. Make sure to use the box drawing characters from assignment 2.
Task 5
Implement the move related method in the Entity & GameSate classes.
Task 5A
Create the Player class and spawn the player on the map and get the movement keys working.
Task 6
Implement the entities, I would recommend starting with easy ones first like the cactus and flag. The Goblin is the hardest, do it last.
Make sure each monster gives an appropriate message when the player collides with them.
Task 7
Design a map/level of your own design, the starting positions of all the entities should be fixed (not random) you must use at least one of each entity type.
Task 8 - Bonus Optional
Implement coloured sprites into your game.
You can do this by printing the special colour code escape sequences.
The code below will change the font colour of the terminal (putty) to bright yellow.
System.out.printf("\033[38;5;%dm", 11);
The number (11) on the right corresponds with the colour codes for yellow in the table below. You can also the background of the font to green using:
System.out.printf("\033[48;5;%dm", 10);
Make sure that you set the font back to normal when you finish printing using the reset code:
System.out.print("\033[0m");
I recommend that you set the colour, print the sprite, then immediately reset the colour to avoid any funky colour issues. It would be a good idea to do the colour stuff in the drawSpriteAt() method.
I also recommend making a Colour enum to contain all the colour codes you want to use. Then you could pass the Colour object into drawSpriteAt() to set the colour.
The enum could then have methods getForegroundCode() and getBackgroundCode() which would return the appropriate string.
This would allow you to do
String code = Colours.RED.getForegroundCode();
I gave a short example of this in the Week 10 lecture videos.
Task 9 – Bonus Optional
Implement an entity of your own design. Some ideas:
- A collectable, like coins in Mario. Keep track of how many collectables have been collected and display them on screen. Make sure to delete the collectable from the entities array when they are picked up using removeEntity()
- A monster that hugs the left
- A door that can be opened & closed by standing on a button. Like the green buttons in Chips
- A teleporter, an entity with two sprites at two different locations on the map. When the player collides with one, they are teleported to the
You will need to implement some extra methods in Entity to allow teleporting entities.
Here is a sample of what the game could look like, you are expect you to design a more interesting &challenging level. The border uses “Box Drawing Light” characters.
Appendix – What is GameThreaded.java?
This is a driver program that uses multiple threads to allow the monsters to move without your input. If you have implemented GameState.java correctly it should just work.
Threads are an advanced concept that is not covered in CSE1OOF.
You are not expected to use or understand GameThreaded.java.
I have included it here so you can improve your game outside of CSE1OOF. We will be using GameDriver.java to mark your program.
More detailed explanation:
In CSE1OOF we have been using a single “thread”, basically the JVM moves though your code line by
line down the page, jumping in and out of methods as required.
However, this means it can only do one thing at a time, if you call Scanner.nextLine() the JVM must sit there and wait for it to return before the rest of the code can be executed. This is known as a blocking operation.
Additionally, a single thread is limited to only one CPU core, your code will never be able to use multiple CPU cores.
Multithreading is when you have multiple thread which are each able to execute code independently from the other threads. So, if one thread is blocking or busy the other threads are unaffected. Each thread gets its own copy of local variables, but they share object attributes.
Multithreading can be tricky because care needs to be taken to not have multiple thread editing the same data at the same time. Failing to do so leads to race conditions which is where the outcome of a program changes depending on which thread got to it first. Race conditions are extremely hard to debug.
In Java you can use synchronized() blocks to prevent multiple thread from being inside the same section of code.