The project I will use for this lecture is my Java 2D game, available here: [https://github.com/Montekkundan/2D-game](https://github.com/Montekkundan/2D-game). I originally built this project a few years ago while following this [YouTube playlist](https://www.youtube.com/playlist?list=PL_QPQmz5C6WUF-pOQDsbsKbaBZqXj4qSq). Hi everyone, my name is [Montek](https://montek.dev), and today I want to talk about object-oriented programming through a small Java game I built when I was learning Java. When we first learn programming, most examples are intentionally small. We write a method, test a loop, make an array, or create one simple class. That is useful, but it can also make OOP feel like a set of definitions instead of a way to organize a real program. A few years ago, I wanted to move beyond isolated exercises and build something that felt like an actual application. I followed a YouTube tutorial series and created a small 2D game. What helped me was not only getting a character to move on the screen. It was seeing why a Java project needs separate classes, why objects need to talk to each other, and why organization starts to matter once the code grows beyond a few hundred lines. This project connects to many concepts from COMP 155: classes, objects, inheritance, abstraction, collections, graphical user interfaces, event handling, collision detection, and game loops. The game itself is simple, but that is actually useful for teaching. The code is large enough to show real structure, but small enough that we can still understand the main pieces in one lecture. ## 1. Why OOP matters in a game Let us begin with a simple comparison. If we wanted to create a calculator, we could probably write most of the logic inside a small number of methods. The program receives input, performs a calculation, displays a result, and waits for the next input. A game is different. A game is not one task. It is a small world that keeps running while the program is active. Consider everything that exists inside even a simple 2D game. There is a player character that can move around the world, non-player characters that interact with the player, monsters with their own behaviors, weapons and collectibles, maps and environments, collision systems, keyboard input, graphics, sound effects, and a game loop that updates everything many times per second. If all of this functionality were placed inside a single Java file, the code would quickly become difficult to read, difficult to debug, and almost impossible to maintain. This is the kind of problem where object-oriented programming becomes practical. Instead of treating the whole game as one giant file, we divide it into pieces. One class can represent the player. Another can represent an NPC. Another can handle collision checking. Another can place objects into the world. The important idea is not just "use classes." The important idea is that each class should have a job the reader can understand. ### 1.1 What is the opposite of OOP? When people talk about the opposite of object-oriented programming, they usually mean procedural programming, or sometimes a functional style. In procedural programming, the focus is on functions and data rather than objects. Instead of creating a `Player` object with its own data and behavior, we might store player information in variables and write separate functions that operate on those variables. For small programs, procedural programming can work very well. Many introductory programming examples are written this way because they are simple and easy to understand. However, as programs become larger, it becomes harder to keep track of which functions modify which data. For example, a procedural version of a player might look like this: ```java int playerX = 100; int playerY = 100; int playerHealth = 10; public static void movePlayer() { playerX += 5; } public static void damagePlayer(int amount) { playerHealth -= amount; } ``` This version works for a tiny example. The problem is what happens when the program grows. If we do the same thing for monsters, NPCs, projectiles, and items, the code becomes a long list of variables and functions that are hard to keep straight. With OOP, we can group the data and behavior together: ```java public class Player { private int x = 100; private int y = 100; private int health = 10; public void move() { x += 5; } public void takeDamage(int amount) { health -= amount; } } ``` Now the player's data and behavior live in one place. When I read this version, I know where to look if I want to understand the player. That is one of the main reasons OOP feels natural in a game project. ### 1.2 Why this game needed OOP This game contains many different kinds of objects: the player, NPCs, monsters, weapons, projectiles, maps, and interactive tiles. All of them have their own state and behavior. Without OOP, we would likely need large functions that constantly check what type of thing they are dealing with. The code would become difficult to follow quickly. OOP allowed the project to create a common `Entity` class and then build specialized classes such as `Player`, `Npc_OldMan`, and the monster classes. Instead of rewriting movement, collision, health, and position logic repeatedly, those features could be shared through inheritance and abstraction. For this project, OOP was not just a formal requirement. It made the code easier to reason about. ### 1.3 Do all companies use OOP today? Most software companies use object-oriented programming to some extent because many common languages support it, including Java, C#, C++, Python, and Kotlin. At the same time, professional code is rarely "pure OOP" from top to bottom. Real projects usually mix styles depending on the problem. For example, a Java application might use classes and objects for its overall architecture, then use streams and lambdas for collection processing. So learning OOP is still very useful, especially in Java, but it should not be treated as the only way to think about programming. ### 1.4 Is all code OOP now? No. OOP is extremely common, but not all code is written that way. Languages such as C are still widely used in operating systems, embedded systems, networking software, and performance-critical applications. Functional programming is also still used. Languages such as Haskell, Elixir, Scala, F#, and Clojure emphasize functional concepts, and Java has included functional features since Java 8. The practical lesson is that programming styles are tools. OOP is one important tool, not the whole toolbox. ### 1.5 Does anyone still use functional programming in Java? Yes. Functional programming is used regularly in modern Java. Java Streams, for example, let developers process collections without writing a traditional loop: ```java List<Integer> numbers = List.of(1, 2, 3, 4, 5); numbers.stream() .filter(n -> n % 2 == 0) .forEach(System.out::println); ``` This code uses a functional style because it describes the transformation we want instead of manually controlling every step. In many Java projects, the larger architecture is object-oriented, while smaller operations like filtering, mapping, and collecting data use functional techniques. ## 2. The main engine: GamePanel In this project, the central class is called `GamePanel`. When I look at the code, this is the class that feels like the engine room of the game. It brings together the screen settings, world settings, frame rate, game thread, player, NPCs, monsters, objects, projectiles, and drawing logic. ```java public class GamePanel extends JPanel implements Runnable { // Screen Settings final int originalTileSize = 16; final int scale = 3; public final int tileSize = originalTileSize * scale; public final int maxScreenColumn = 20; public final int maxScreenRow = 12; public final int screenWidth = tileSize * maxScreenColumn; public final int screenHeight = tileSize * maxScreenRow; int FPS = 60; Thread gameThread; } ``` There are two important details here. First, `GamePanel` extends `JPanel`, which means it inherits functionality from Java Swing. Because it is a panel, it can draw graphics and become part of a graphical user interface. Second, it implements `Runnable`, which allows the game to execute inside its own thread. A game needs a process that keeps updating and redrawing while the application is active. Even in this small fragment, we can see why a class is more than a container for random code. `GamePanel` holds state, such as screen dimensions and frame rate, and it also owns behavior related to running and drawing the game. ## 3. Managing a world of game objects Inside `GamePanel`, we find references to many different game elements. This is where the project stops feeling like a small exercise and starts feeling like a system. ```java public Player player = new Player(this, keyH); public Entity obj[][] = new Entity[maxMap][100]; public Entity npc[][] = new Entity[maxMap][10]; public Entity monster[][] = new Entity[maxMap][20]; public ArrayList<Entity> projectileList = new ArrayList<>(); public ArrayList<Entity> particleList = new ArrayList<>(); ArrayList<Entity> entityList = new ArrayList<>(); ``` When students first learn classes and objects, the examples are often very small. We might create one `Student` object or one `Car` object. In a game, the reason for objects becomes more obvious because many things exist at the same time. The player has a position, health, inventory, and movement logic. An NPC has dialogue, animations, and movement patterns. A monster has health, attack behavior, and collision logic. A projectile has a direction, speed, and lifespan. Particles may exist only briefly, but they still require data and behavior. Each of these elements represents something different, yet they all need to be updated and drawn while the game is running. This is where collections become important. Rather than creating variables such as `monster1`, `monster2`, and `monster3`, the game stores groups of objects inside arrays and `ArrayList`. The pattern scales much better. Whether the game contains five monsters or five hundred monsters, the loop that updates and draws them can keep the same basic shape. ## 4. The game loop: update and repaint One of the most important ideas in game development is the game loop. Unlike a program that runs once and exits, a game keeps asking the same questions: What input is happening? What changed in the world? What should be drawn now? The loop in this project looks like this: ```java public void run() { double drawInterval = 1000000000 / FPS; double delta = 0; long lastTime = System.nanoTime(); long currentTime; while(gameThread != null) { currentTime = System.nanoTime(); delta += (currentTime - lastTime) / drawInterval; lastTime = currentTime; if(delta >= 1) { update(); repaint(); delta--; } } } ``` At first glance, this code may look technical because it uses time calculations and `System.nanoTime()`. But the concept is simple. The game updates the world, repaints the screen, and then repeats. This cycle occurs approximately sixty times per second, creating the illusion of continuous movement. When the player presses a key, the game detects the input, updates the player's position, and redraws the scene. Because this happens many times every second, movement appears smooth. This pattern also shows up outside games. Simulation software, robotics systems, and some AI systems use a similar cycle of observing, updating, and acting. The game loop gives us a concrete version of that idea. ## 5. The Entity class: abstraction One of the most useful design decisions in this project is the `Entity` class. To understand why this class exists, ask a simple question: what do a player, an NPC, and a monster have in common? ```java public class Entity { GamePanel gp; public int worldX, worldY; public String direction = "down"; public int speed; public int maxLife; public int life; public int attack; public int defence; public boolean collisionOn = false; public Entity(GamePanel gp) { this.gp = gp; } public void setAction() {} public void update() { setAction(); collisionOn = false; gp.cChecker.checkTile(this); gp.cChecker.checkObject(this, false); gp.cChecker.checkEntity(this, gp.npc); gp.cChecker.checkEntity(this, gp.monster); } } ``` Although players, NPCs, and monsters serve different purposes, they share a lot. They have positions in the world, movement directions, collision interactions, and some form of state. Instead of rewriting these features repeatedly, the project creates a general abstraction called `Entity`. An abstraction is a simplified representation of a broader idea. Here, we are not starting with what makes the player special. We are starting with what all game entities need. The `Entity` class becomes a blueprint for anything that exists in the game world. This reduces duplication. If we change how entities handle collision checks, we can often make that change in one place instead of chasing the same logic across many classes. ## 6. Player extends Entity The player is represented by a class called `Player`. This class is not written completely from scratch. Instead, it builds on top of the more general `Entity` class. ```java public class Player extends Entity { GamePanel gp; KeyHandler keyH; public ArrayList<Entity> inventory = new ArrayList<>(); public final int maxInventorySize = 20; public Player(GamePanel gp, KeyHandler keyH) { super(gp); this.gp = gp; this.keyH = keyH; setDefaultValues(); getPlayerImage(); getPlayerAttackImage(); setItems(); } } ``` The key phrase here is `extends Entity`. This is inheritance. The player already needs what `Entity` provides: position, movement, collision handling, and health-related attributes. Rather than rewriting those features, the player inherits them. The `Player` class then adds what is unique to the player: keyboard controls, inventory management, equipment systems, attack animations, and player-specific interactions. Textbook examples often use something like `Dog extends Animal`. That example is technically correct, but a game makes the relationship easier to see. The player is clearly a specialized kind of entity. ## 7. NPC also extends Entity The same principle applies to NPCs. In this project, the old man NPC also extends `Entity`, which means he inherits the common structure shared by game entities while still defining his own specific behavior. ```java public class Npc_OldMan extends Entity { public Npc_OldMan(GamePanel gp) { super(gp); direction = "down"; speed = 1; getNpcImage(); setDialogue(); } } ``` The old man NPC inherits all of the common functionality provided by `Entity`. However, he also has unique dialogue, animations, and behaviors. For example, the following method determines how the NPC moves around the world: ```java public void setAction() { actionLockCounter++; if (actionLockCounter == 120) { Random random = new Random(); int i = random.nextInt(100) + 1; if (i <= 25) { direction = "up"; } if (i > 25 && i <= 50) { direction = "down"; } if (i > 50 && i <= 75) { direction = "left"; } if (i > 75 && i <= 100) { direction = "right"; } actionLockCounter = 0; } } ``` This method is a simple form of behavior. Every so often, the NPC randomly selects a new direction. The important point is that the old man still fits into the same entity system as the player and monsters, but he can define his own behavior. That is the practical value of inheritance here. We reuse what is common, specialize what is different, and keep the design understandable. ## 8. Separation of responsibilities: AssetSetter Another useful class in the project is `AssetSetter`. It is not the most exciting class in the game, but it is a good example of separation of responsibilities. ```java public void setNpc() { int mapNum = 0; int i = 0; gp.npc[mapNum][i] = new Npc_OldMan(gp); gp.npc[mapNum][i].worldX = gp.tileSize * 21; gp.npc[mapNum][i].worldY = gp.tileSize * 21; i++; } ``` The purpose of `AssetSetter` is not to manage gameplay, render graphics, or process keyboard input. Its job is to place objects into the game world. In this example, it creates an old man NPC and places him at a specific location on the map. This is a small example, but it is a habit that matters in larger projects. Good software architecture is often less about clever code and more about giving each part of the program a clear responsibility. ## 9. Collision detection: practical logic A game world needs rules. One of the simplest rules is that characters should not walk through walls. Collision detection enforces that rule by checking where an entity is, where it is trying to move, and whether the next tile or object should block that movement. ```java public void checkTile(Entity entity) { int entityLeftWorldX = entity.worldX + entity.solidArea.x; int entityRightWorldX = entity.worldX + entity.solidArea.x + entity.solidArea.width; int entityTopWorldY = entity.worldY + entity.solidArea.y; int entityBottomWorldY = entity.worldY + entity.solidArea.y + entity.solidArea.height; int entityLeftCol = entityLeftWorldX / gp.tileSize; int entityRightCol = entityRightWorldX / gp.tileSize; int entityTopRow = entityTopWorldY / gp.tileSize; int entityBottomRow = entityBottomWorldY / gp.tileSize; } ``` This code converts world coordinates into tile coordinates. This is useful because the game world is organized as a grid of tiles. Instead of checking every pixel on the screen, the game determines which tiles an entity is touching or about to touch. If the player moves upward, the program checks the tile above the player. If the player moves right, the program checks the tile to the right. If that tile has collision turned on, the entity should not move into that space. This is one reason I like using a game to teach programming. We are not only writing syntax. We are using logic, coordinate systems, geometry, and object-oriented design to create behavior that feels natural to the user. Collision detection is also a good example of object collaboration. The collision checker needs information from the entity, the tile manager, and the game panel. No single class owns the whole behavior by itself. ## 10. How this connects to COMP 155 This project connects directly to many ideas in COMP 155. Classes and objects appear everywhere in the code. `GamePanel`, `Player`, `Entity`, `Npc_OldMan`, `AssetSetter`, and `CollisionChecker` each represent a different part of the game system. Inheritance appears when `Player` and `Npc_OldMan` extend `Entity`. This shows how common behavior can be placed in a parent class and reused by more specialized child classes. Abstraction appears in the `Entity` class because it represents the general idea of something that exists in the game world. Encapsulation appears because each class groups related data and behavior together instead of spreading everything throughout the program. Collections appear through arrays and `ArrayList`, which store groups of objects such as NPCs, monsters, projectiles, and particles. Graphical user interfaces appear through Java Swing, especially because `GamePanel` extends `JPanel`. Object collaboration appears because no single class does everything. The game works because many objects communicate with one another. That is why projects are powerful for learning. They make course concepts visible. When OOP is only a definition, it can feel abstract. When you see a player, monster, NPC, collision checker, and game loop working together, the ideas become easier to place in your mind. ## 11. Connection to modern software, UI components, and AI agents This way of thinking is not limited to Java games. In my current work, I often use technologies such as Next.js, TypeScript, and Drizzle ORM to build modern web applications. The syntax is different, but many of the design habits are familiar. In TypeScript, we use types and interfaces to describe the shape of data. With an ORM like Drizzle, we define database schemas and interact with data through structured abstractions instead of writing everything manually in raw SQL. We are still trying to create clear structures and make complex systems easier to work with. This also appears in user interface development. If you are fascinated by UI design like I am, websites such as [https://www.components.build/](https://www.components.build/) show how modern interfaces are often built from reusable components. A button, card, input field, navbar, or modal can be designed once and reused across an application. In React or Next.js, we might use a component like `<Button />`. Behind that simple tag, there may be styling, behavior, variants, accessibility, and interaction logic. That is not identical to Java OOP, but the instinct is similar: create reusable building blocks instead of rewriting the same idea everywhere. Today, people also talk a lot about AI agents. At first, AI agents may sound completely different from a Java 2D game. However, from a software engineering point of view, there are some interesting similarities. An AI agent usually has state, memory, actions, tools, and an environment. It observes something, decides what to do, performs an action, and then repeats this process. A game character also has state, actions, and an environment. It exists inside a world, responds to rules, updates over time, and interacts with other objects. In this project, the old man NPC randomly chooses a direction after a certain amount of time. That is obviously not advanced intelligence, but it is still a simple behavior system. It shows how an object can store state, make a decision based on logic, and act inside an environment. Of course, a simple NPC that randomly changes direction is not the same as a modern AI system. The point is smaller and more useful: the structure teaches us how to represent something with state, behavior, actions, and an environment. These ideas appear in simulations, robotics, software tools, AI systems, web applications, UI libraries, and large application architectures. A robotics system may have sensors, controllers, actuators, and an environment. A web application may have components, database models, server functions, and API routes. An AI agent may have memory, tools, planning logic, and action execution. A game may have entities, maps, collisions, and update loops. The details change, but the habit of breaking a system into understandable parts remains useful. Learning OOP through a game is valuable because it gives us a visual, practical way to understand how systems are built before we move on to larger software. ## 12. Conclusion: what students should remember My main message is this: do not learn object-oriented programming only by memorizing definitions. Abstraction, encapsulation, inheritance, and polymorphism matter, but they become much easier to understand when you use them to build something. When I built this 2D Java game, I started understanding classes as real components, not just textbook examples. I could see why a `Player` class should exist, why an `Entity` class was useful, why collections mattered, and why separating responsibilities made the code easier to manage. That mindset helped me later when I worked on larger software systems, AI tools, and research projects. Whether you are building a game, a web application, an AI tool, a UI component library, a database system, or a simulation, you still need to think about state, behavior, responsibility, and interaction between components. So for COMP 155, whenever you learn a new concept, ask yourself where it would appear in a real project. When you learn classes, think about what object the class represents. When you learn inheritance, think about what common behavior can be reused. When you learn collections, think about what group of objects your program needs to manage. When you learn GUI, think about how the user interacts with the program. When you learn file I/O, think about what information your program needs to save or load. If you build with that mindset, you will not only understand Java syntax. You will understand how to design software systems. That is the real value of object-oriented programming, and that is why a small project like this game can become a powerful learning tool.