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
+160
View File
@@ -0,0 +1,160 @@
---
title: Component
categories: Behavioral
language: en
tags:
- Game programming
- Domain
---
## Intent
The component design pattern enables developers to decouple attributes of an objects. Essentially allowing a single
component to be inheritable by multiple domains/objects without linking the objects to each other. In addition to this
benefit, the component design pattern allows developer to write maintainable and comprehensible code which is less
likely to result in monolithic classes.
![Intent](./etc/component.duplication.png "Component Design Pattern")
## Explanation
Real world example
> Suppose your video game consists of a graphics component and a sound component. Including the methods and attributes of both of these features in a single java class can be problematic due to many reasons. Firstly, the graphics and sound code can create an extremely long java class which can be hard to maintain. Furthermore, graphics components may be written and implemented by a separate team as to the sound contents. If both parties work simultaneously on the same java class, this may cause conflicts and major delay. Using the component design pattern, the development team is able to create individual component classes for graphics and sound whilst providing the domain/object the reach to both of these attributes.
In plain words
> The component design pattern provides a single attribute to be accessible by numerous objects without requiring the
> existence of a relationship between the objects themselves.
Key drawback
> With the implementation of the component design pattern, it can be very difficult to create a relationship
> between components. For example, suppose we require the sound component to be aware of the current animation in order
> create a certain sound based upon the animation; this can be quite tricky as the component design pattern makes
> components 'unaware' of other components' existence due to its decoupling nature.
**Programmatic Example**
The App class creates a demonstration of the use of the component pattern by creating two different objects which
inherit a small collection of individual components that are modifiable.
```java
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();
}
}
```
Much of the program exists within the GameObject class, within this class, the player and NPC object create methods are
set up. Additionally, this class also consists of the method calls used to update/alter information of the object's
components.
```java
public class GameObject {
private final InputComponent inputComponent;
private final PhysicComponent physicComponent;
private final GraphicComponent graphicComponent;
public String name;
public int velocity = 0;
public int coordinate = 0;
public static GameObject createPlayer() {
return new GameObject(new PlayerInputComponent(),
new ObjectPhysicComponent(),
new ObjectGraphicComponent(),
"player");
}
public static GameObject createNpc() {
return new GameObject(
new DemoInputComponent(),
new ObjectPhysicComponent(),
new ObjectGraphicComponent(),
"npc");
}
public void demoUpdate() {
inputComponent.update(this);
physicComponent.update(this);
graphicComponent.update(this);
}
public void update(int e) {
inputComponent.update(this, e);
physicComponent.update(this);
graphicComponent.update(this);
}
public void updateVelocity(int acceleration) {
this.velocity += acceleration;
}
public void updateCoordinate() {
this.coordinate += this.velocity;
}
}
```
Upon opening the component package, the collection of components are revealed. These components provide the interface
for objects to inherit these domains. The PlayerInputComponent class shown below updates the object's velocity
characteristic based on user's key event input.
```java
public class PlayerInputComponent implements InputComponent {
private static final int walkAcceleration = 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.");
}
case KeyEvent.KEY_LOCATION_RIGHT -> {
gameObject.updateVelocity(WALK_ACCELERATION);
LOGGER.info(gameObject.getName() + " has moved right.");
}
default -> {
LOGGER.info(gameObject.getName() + "'s velocity is unchanged due to the invalid input");
gameObject.updateVelocity(0);
} // incorrect input
}
}
}
```
## Class diagram
![UML](./etc/component.uml.png "The UML for Component Design Pattern")
## Applicability
Use the component design pattern when
- you have a class which access multiple features which you would like to keep separate.
- you want to reduce the length of a class.
- you require a variety of objects to share a collection of components but the use of inheritance isn't specific enough.
## Credits
- [Component Design Pattern] (https://gameprogrammingpatterns.com/component.html)
- [Component pattern - game programming series - Tutemic] (https://www.youtube.com/watch?v=n92GBp2WMkg&ab_channel=Tutemic)
Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+77
View File
@@ -0,0 +1,77 @@
@startuml
class App
class GameObject
interface GraphicComponent
interface InputComponent
interface PhysicComponent
class ObjectGraphicComponent
class DemoInputComponent
class PlayerInputComponent
class ObjectPhysicComponent
GraphicComponent <|.. ObjectGraphicComponent
InputComponent <|.. DemoInputComponent
InputComponent <|.. PlayerInputComponent
PhysicComponent <|.. ObjectPhysicComponent
GameObject *-- ObjectGraphicComponent
GameObject *.. DemoInputComponent
GameObject *.. PlayerInputComponent
GameObject *-- ObjectPhysicComponent
class App {
+main(String[] args)
}
class GameObject{
- inputComponent;
- physicComponent;
- graphicComponent;
- name;
- velocity
- coordinate
+GameObject()
+createPlayer()
+createNpc()
+demoUpdate()
+update(e:int)
+getName()
+getVelocity()
+setVelocity(acceleration:int)
+getCoordinate()
+setCoordinate()
}
interface GraphicComponent{
+update()
}
interface InputComponent{
+update()
}
interface PhysicComponent{
+update()
}
class ObjectGraphicComponent{
+update(gameObject:GameObject)
}
class DemoInputComponent{
-walkAcceleration
+update(gameObject:GameObject,e:int)
}
class PlayerInputComponent{
-walkAcceleration
+update(gameObject:GameObject,e:int)
}
class ObjectPhysicComponent{
+update(gameObject:GameObject)
}
@enduml
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>java-design-patterns</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.26.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>component</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<configuration>
<archive>
<manifest>
<mainClass>com.iluwatar.component.App</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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());
}
}