diff --git a/component/README.md b/component/README.md
new file mode 100644
index 000000000..c8808e396
--- /dev/null
+++ b/component/README.md
@@ -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.
+
+
+
+## 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
+
+
+
+## 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)
\ No newline at end of file
diff --git a/component/etc/component.duplication.png b/component/etc/component.duplication.png
new file mode 100644
index 000000000..1e5f376f3
Binary files /dev/null and b/component/etc/component.duplication.png differ
diff --git a/component/etc/component.uml.png b/component/etc/component.uml.png
new file mode 100644
index 000000000..44b789453
Binary files /dev/null and b/component/etc/component.uml.png differ
diff --git a/component/etc/component.uml.puml b/component/etc/component.uml.puml
new file mode 100644
index 000000000..1c386fa30
--- /dev/null
+++ b/component/etc/component.uml.puml
@@ -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
\ No newline at end of file
diff --git a/component/pom.xml b/component/pom.xml
new file mode 100644
index 000000000..d416ae4f5
--- /dev/null
+++ b/component/pom.xml
@@ -0,0 +1,39 @@
+
+
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. + * + *
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(); + } +} diff --git a/component/src/main/java/com/iluwatar/component/GameObject.java b/component/src/main/java/com/iluwatar/component/GameObject.java new file mode 100644 index 000000000..10410c18e --- /dev/null +++ b/component/src/main/java/com/iluwatar/component/GameObject.java @@ -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. + * + *
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; + } +} diff --git a/component/src/main/java/com/iluwatar/component/component/graphiccomponent/GraphicComponent.java b/component/src/main/java/com/iluwatar/component/component/graphiccomponent/GraphicComponent.java new file mode 100644 index 000000000..26a80cc7a --- /dev/null +++ b/component/src/main/java/com/iluwatar/component/component/graphiccomponent/GraphicComponent.java @@ -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); +} diff --git a/component/src/main/java/com/iluwatar/component/component/graphiccomponent/ObjectGraphicComponent.java b/component/src/main/java/com/iluwatar/component/component/graphiccomponent/ObjectGraphicComponent.java new file mode 100644 index 000000000..b147a916d --- /dev/null +++ b/component/src/main/java/com/iluwatar/component/component/graphiccomponent/ObjectGraphicComponent.java @@ -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()); + } +} diff --git a/component/src/main/java/com/iluwatar/component/component/inputcomponent/DemoInputComponent.java b/component/src/main/java/com/iluwatar/component/component/inputcomponent/DemoInputComponent.java new file mode 100644 index 000000000..34e0e1202 --- /dev/null +++ b/component/src/main/java/com/iluwatar/component/component/inputcomponent/DemoInputComponent.java @@ -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. + * + *
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.");
+ }
+}
diff --git a/component/src/main/java/com/iluwatar/component/component/inputcomponent/InputComponent.java b/component/src/main/java/com/iluwatar/component/component/inputcomponent/InputComponent.java
new file mode 100644
index 000000000..a59708603
--- /dev/null
+++ b/component/src/main/java/com/iluwatar/component/component/inputcomponent/InputComponent.java
@@ -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);
+}
diff --git a/component/src/main/java/com/iluwatar/component/component/inputcomponent/PlayerInputComponent.java b/component/src/main/java/com/iluwatar/component/component/inputcomponent/PlayerInputComponent.java
new file mode 100644
index 000000000..00b66e5bd
--- /dev/null
+++ b/component/src/main/java/com/iluwatar/component/component/inputcomponent/PlayerInputComponent.java
@@ -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
+ }
+ }
+}
diff --git a/component/src/main/java/com/iluwatar/component/component/physiccomponent/ObjectPhysicComponent.java b/component/src/main/java/com/iluwatar/component/component/physiccomponent/ObjectPhysicComponent.java
new file mode 100644
index 000000000..2fc4f53a7
--- /dev/null
+++ b/component/src/main/java/com/iluwatar/component/component/physiccomponent/ObjectPhysicComponent.java
@@ -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.");
+ }
+}
diff --git a/component/src/main/java/com/iluwatar/component/component/physiccomponent/PhysicComponent.java b/component/src/main/java/com/iluwatar/component/component/physiccomponent/PhysicComponent.java
new file mode 100644
index 000000000..d7f55153f
--- /dev/null
+++ b/component/src/main/java/com/iluwatar/component/component/physiccomponent/PhysicComponent.java
@@ -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);
+}
diff --git a/component/src/test/java/com/iluwatar/component/AppTest.java b/component/src/test/java/com/iluwatar/component/AppTest.java
new file mode 100644
index 000000000..35b5f243f
--- /dev/null
+++ b/component/src/test/java/com/iluwatar/component/AppTest.java
@@ -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[]{}));
+ }
+}
diff --git a/component/src/test/java/com/iluwatar/component/GameObjectTest.java b/component/src/test/java/com/iluwatar/component/GameObjectTest.java
new file mode 100644
index 000000000..68a353212
--- /dev/null
+++ b/component/src/test/java/com/iluwatar/component/GameObjectTest.java
@@ -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());
+ }
+}
diff --git a/pom.xml b/pom.xml
index dfbd1f62a..01a117c1d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -213,6 +213,7 @@