feature: Completed component design pattern implementation, testing and respective README (#2153)

* update the explanation in README.md

* #556 update initial files

* added README to component design pattern

* Rearrange the file

* Finalize the directory

* Add test sample

* Update the title for README.md

* Update the title for README.md

* Update the title for README.md

* Update the title for README.md

* Finish the component design pattern

* Updated comments and docstrings for component DP, added basic tests for App and GameObject java classes. Slight modifications to pom.xml to reflect the test suite introduction.

* updated comments/docstrings for all classes - wrote v1 of README.md for component design pattern.

This still requires the class diagram sketch.

* Update the UML and linked with the README.md

* Update the README.md

* Remove the additional update method and rearrange the file based on the CheckStyle plugin

* Changed the structure based on the code smells feedback from PR

* Documentation update - uml

* Documentation update - grammar

* Updated readme to reflect the use of the LOGGER instead of the system output prints.

* Correct the constant name

* Uses Lombok to remove getter/setter boilerplate

* Rename the constant name

* Branch out from master and finish all the review changes

* Correct the CheckStyle warning

Co-authored-by: Samman Palihapitiya Gamage <u7287889@anu.edu.au>
Co-authored-by: SammanPali <110753804+SammanPali@users.noreply.github.com>
This commit is contained in:
Qixiang Chen
2022-11-21 00:19:26 +11:00
committed by GitHub
parent 0a53b23c61
commit 1a14fa4f40
17 changed files with 636 additions and 0 deletions
@@ -0,0 +1,38 @@
package com.iluwatar.component;
import java.awt.event.KeyEvent;
import lombok.extern.slf4j.Slf4j;
/**
* The component design pattern is a common game design structure. This pattern is often
* used to reduce duplication of code as well as to improve maintainability.
* In this implementation, component design pattern has been used to provide two game
* objects with varying component interfaces (features). As opposed to copying and
* pasting same code for the two game objects, the component interfaces allow game
* objects to inherit these components from the component classes.
*
* <p>The implementation has decoupled graphic, physics and input components from
* the player and NPC objects. As a result, it avoids the creation of monolithic java classes.
*
* <p>The below example in this App class demonstrates the use of the component interfaces
* for separate objects (player & NPC) and updating of these components as per the
* implementations in GameObject class and the component classes.
*/
@Slf4j
public final class App {
/**
* Program entry point.
*
* @param args args command line args.
*/
public static void main(String[] args) {
final var player = GameObject.createPlayer();
final var npc = GameObject.createNpc();
LOGGER.info("Player Update:");
player.update(KeyEvent.KEY_LOCATION_LEFT);
LOGGER.info("NPC Update:");
npc.demoUpdate();
}
}
@@ -0,0 +1,94 @@
package com.iluwatar.component;
import com.iluwatar.component.component.graphiccomponent.GraphicComponent;
import com.iluwatar.component.component.graphiccomponent.ObjectGraphicComponent;
import com.iluwatar.component.component.inputcomponent.DemoInputComponent;
import com.iluwatar.component.component.inputcomponent.InputComponent;
import com.iluwatar.component.component.inputcomponent.PlayerInputComponent;
import com.iluwatar.component.component.physiccomponent.ObjectPhysicComponent;
import com.iluwatar.component.component.physiccomponent.PhysicComponent;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* The GameObject class has three component class instances that allow
* the creation of different game objects based on the game design requirements.
*/
@Getter
@RequiredArgsConstructor
public class GameObject {
private final InputComponent inputComponent;
private final PhysicComponent physicComponent;
private final GraphicComponent graphicComponent;
private final String name;
private int velocity = 0;
private int coordinate = 0;
/**
* Creates a player game object.
*
* @return player object
*/
public static GameObject createPlayer() {
return new GameObject(new PlayerInputComponent(),
new ObjectPhysicComponent(),
new ObjectGraphicComponent(),
"player");
}
/**
* Creates a NPC game object.
*
* @return npc object
*/
public static GameObject createNpc() {
return new GameObject(
new DemoInputComponent(),
new ObjectPhysicComponent(),
new ObjectGraphicComponent(),
"npc");
}
/**
* Updates the three components of the NPC object used in the demo in App.java
* note that this is simply a duplicate of update() without the key event for
* demonstration purposes.
*
* <p>This method is usually used in games if the player becomes inactive.
*/
public void demoUpdate() {
inputComponent.update(this, 0);
physicComponent.update(this);
graphicComponent.update(this);
}
/**
* Updates the three components for objects based on key events.
*
* @param e key event from the player.
*/
public void update(int e) {
inputComponent.update(this, e);
physicComponent.update(this);
graphicComponent.update(this);
}
/**
* Update the velocity based on the acceleration of the GameObject.
*
* @param acceleration the acceleration of the GameObject
*/
public void updateVelocity(int acceleration) {
this.velocity += acceleration;
}
/**
* Set the c based on the current velocity.
*/
public void updateCoordinate() {
this.coordinate += this.velocity;
}
}
@@ -0,0 +1,10 @@
package com.iluwatar.component.component.graphiccomponent;
import com.iluwatar.component.GameObject;
/**
* Generic GraphicComponent interface.
*/
public interface GraphicComponent {
void update(GameObject gameObject);
}
@@ -0,0 +1,21 @@
package com.iluwatar.component.component.graphiccomponent;
import com.iluwatar.component.GameObject;
import lombok.extern.slf4j.Slf4j;
/**
* ObjectGraphicComponent class mimics the graphic component of the Game Object.
*/
@Slf4j
public class ObjectGraphicComponent implements GraphicComponent {
/**
* The method updates the graphics based on the velocity of gameObject.
*
* @param gameObject the gameObject instance
*/
@Override
public void update(GameObject gameObject) {
LOGGER.info(gameObject.getName() + "'s current velocity: " + gameObject.getVelocity());
}
}
@@ -0,0 +1,28 @@
package com.iluwatar.component.component.inputcomponent;
import com.iluwatar.component.GameObject;
import lombok.extern.slf4j.Slf4j;
/**
* Take this component class to control player or the NPC for demo mode.
* and implemented the InputComponent interface.
*
* <p>Essentially, the demo mode is utilised during a game if the user become inactive.
* Please see: http://gameprogrammingpatterns.com/component.html
*/
@Slf4j
public class DemoInputComponent implements InputComponent {
private static final int WALK_ACCELERATION = 2;
/**
* Redundant method in the demo mode.
*
* @param gameObject the gameObject instance
* @param e key event instance
*/
@Override
public void update(GameObject gameObject, int e) {
gameObject.updateVelocity(WALK_ACCELERATION);
LOGGER.info(gameObject.getName() + " has moved right.");
}
}
@@ -0,0 +1,10 @@
package com.iluwatar.component.component.inputcomponent;
import com.iluwatar.component.GameObject;
/**
* Generic InputComponent interface.
*/
public interface InputComponent {
void update(GameObject gameObject, int e);
}
@@ -0,0 +1,38 @@
package com.iluwatar.component.component.inputcomponent;
import com.iluwatar.component.GameObject;
import java.awt.event.KeyEvent;
import lombok.extern.slf4j.Slf4j;
/**
* PlayerInputComponent is used to handle user key event inputs,
* and thus it implements the InputComponent interface.
*/
@Slf4j
public class PlayerInputComponent implements InputComponent {
private static final int WALK_ACCELERATION = 1;
/**
* The update method to change the velocity based on the input key event.
*
* @param gameObject the gameObject instance
* @param e key event instance
*/
@Override
public void update(GameObject gameObject, int e) {
switch (e) {
case KeyEvent.KEY_LOCATION_LEFT:
gameObject.updateVelocity(-WALK_ACCELERATION);
LOGGER.info(gameObject.getName() + " has moved left.");
break;
case KeyEvent.KEY_LOCATION_RIGHT:
gameObject.updateVelocity(WALK_ACCELERATION);
LOGGER.info(gameObject.getName() + " has moved right.");
break;
default:
LOGGER.info(gameObject.getName() + "'s velocity is unchanged due to the invalid input");
gameObject.updateVelocity(0);
break; // incorrect input
}
}
}
@@ -0,0 +1,22 @@
package com.iluwatar.component.component.physiccomponent;
import com.iluwatar.component.GameObject;
import lombok.extern.slf4j.Slf4j;
/**
* Take this component class to update the x coordinate for the Game Object instance.
*/
@Slf4j
public class ObjectPhysicComponent implements PhysicComponent {
/**
* The method update the horizontal (X-axis) coordinate based on the velocity of gameObject.
*
* @param gameObject the gameObject instance
*/
@Override
public void update(GameObject gameObject) {
gameObject.updateCoordinate();
LOGGER.info(gameObject.getName() + "'s coordinate has been changed.");
}
}
@@ -0,0 +1,10 @@
package com.iluwatar.component.component.physiccomponent;
import com.iluwatar.component.GameObject;
/**
* Generic PhysicComponent interface.
*/
public interface PhysicComponent {
void update(GameObject gameObject);
}
@@ -0,0 +1,17 @@
package com.iluwatar.component;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/**
* Tests App class : src/main/java/com/iluwatar/component/App.java
* General execution test of the application.
*/
class AppTest {
@Test
void shouldExecuteComponentWithoutException() {
assertDoesNotThrow(() -> App.main(new String[]{}));
}
}
@@ -0,0 +1,71 @@
package com.iluwatar.component;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.awt.event.KeyEvent;
import lombok.extern.slf4j.Slf4j;
/**
* Tests GameObject class.
* src/main/java/com/iluwatar/component/GameObject.java
*/
@Slf4j
class GameObjectTest {
GameObject playerTest;
GameObject npcTest;
@BeforeEach
public void initEach() {
//creates player & npc objects for testing
//note that velocity and coordinates are initialised to 0 in GameObject.java
playerTest = GameObject.createPlayer();
npcTest = GameObject.createNpc();
}
/**
* Tests the create methods - createPlayer() and createNPC().
*/
@Test
void objectTest(){
LOGGER.info("objectTest:");
assertEquals("player",playerTest.getName());
assertEquals("npc",npcTest.getName());
}
/**
* Tests the input component with varying key event inputs.
* Targets the player game object.
*/
@Test
void eventInputTest(){
LOGGER.info("eventInputTest:");
playerTest.update(KeyEvent.KEY_LOCATION_LEFT);
assertEquals(-1, playerTest.getVelocity());
assertEquals(-1, playerTest.getCoordinate());
playerTest.update(KeyEvent.KEY_LOCATION_RIGHT);
playerTest.update(KeyEvent.KEY_LOCATION_RIGHT);
assertEquals(1, playerTest.getVelocity());
assertEquals(0, playerTest.getCoordinate());
LOGGER.info(Integer.toString(playerTest.getCoordinate()));
LOGGER.info(Integer.toString(playerTest.getVelocity()));
GameObject p2 = GameObject.createPlayer();
p2.update(KeyEvent.KEY_LOCATION_LEFT);
//in the case of an unknown, object stats are set to default
p2.update(KeyEvent.KEY_LOCATION_UNKNOWN);
assertEquals(-1, p2.getVelocity());
}
/**
* Tests the demo component interface.
*/
@Test
void npcDemoTest(){
LOGGER.info("npcDemoTest:");
npcTest.demoUpdate();
assertEquals(2, npcTest.getVelocity());
assertEquals(2, npcTest.getCoordinate());
}
}