├── .gitattributes ├── src ├── res │ ├── Right-Facing-Rifle.png │ ├── Right-Facing-Bullet.png │ ├── Right-Facing-Pistol.png │ ├── standard-map-platform-rectangles.csv │ ├── Right-Facing-Blue-Shooter.png │ └── Right-Facing-Red-Shooter.png └── main │ ├── shooter │ ├── game │ │ ├── entities │ │ │ ├── XAxisType.java │ │ │ ├── YAxisType.java │ │ │ ├── HorDirectionedEntity.java │ │ │ ├── GravitationalEntity.java │ │ │ ├── PlatformEntity.java │ │ │ ├── Vector2D.java │ │ │ ├── TeamedPlayerEntity.java │ │ │ ├── PistolEntity.java │ │ │ ├── KineticEntity.java │ │ │ ├── BulletEntity.java │ │ │ └── PlayerEntity.java │ │ ├── action │ │ │ ├── Action.java │ │ │ └── ActionSet.java │ │ ├── ClientGame.java │ │ ├── ServerTeamGame.java │ │ └── ServerGame.java │ ├── net │ │ ├── packets │ │ │ ├── ServerPacket.java │ │ │ ├── DisconnectPacket.java │ │ │ ├── ClientPacket.java │ │ │ └── ActionPacket.java │ │ ├── ClientHandler.java │ │ ├── Client.java │ │ └── Server.java │ ├── SinglePlayerRunner.java │ ├── gui │ │ ├── client │ │ │ ├── DeathPanel.java │ │ │ ├── ClientMenuPanel.java │ │ │ ├── ClientMainFrame.java │ │ │ ├── ClientInputHandler.java │ │ │ └── ClientGamePanel.java │ │ └── server │ │ │ ├── ServerRunningPanel.java │ │ │ ├── ServerMenuPanel.java │ │ │ └── ServerMainFrame.java │ └── utils │ │ └── ArraySet.java │ └── chatroom │ ├── Server.java │ ├── ClientHandler.java │ └── Client.java └── README.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/res/Right-Facing-Rifle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathkimchi/JavaSocketsGame/HEAD/src/res/Right-Facing-Rifle.png -------------------------------------------------------------------------------- /src/res/Right-Facing-Bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathkimchi/JavaSocketsGame/HEAD/src/res/Right-Facing-Bullet.png -------------------------------------------------------------------------------- /src/res/Right-Facing-Pistol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathkimchi/JavaSocketsGame/HEAD/src/res/Right-Facing-Pistol.png -------------------------------------------------------------------------------- /src/res/standard-map-platform-rectangles.csv: -------------------------------------------------------------------------------- 1 | 10,1,0,0 2 | 1,0.5,2,2.5 3 | 4,1,2,3 4 | 1,10,-0.9,0 5 | 1,10,9.9,0 6 | 1.5,1,8.5,2 -------------------------------------------------------------------------------- /src/res/Right-Facing-Blue-Shooter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathkimchi/JavaSocketsGame/HEAD/src/res/Right-Facing-Blue-Shooter.png -------------------------------------------------------------------------------- /src/res/Right-Facing-Red-Shooter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathkimchi/JavaSocketsGame/HEAD/src/res/Right-Facing-Red-Shooter.png -------------------------------------------------------------------------------- /src/main/shooter/game/entities/XAxisType.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | public enum XAxisType { 4 | LEFT, CENTER, RIGHT; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/YAxisType.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | public enum YAxisType { 4 | TOP, CENTER, BOTTOM; 5 | } 6 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Chat copied off of WittCode: https://www.youtube.com/watch?v=gLfuZrrfKes 2 | 3 | Game inspired by vanZeben: https://www.youtube.com/watch?v=l1p21JWa_8s&list=PL8CAB66181A502179&index=17 -------------------------------------------------------------------------------- /src/main/shooter/net/packets/ServerPacket.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net.packets; 2 | 3 | import java.io.Serializable; 4 | 5 | public class ServerPacket implements Serializable { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/shooter/game/action/Action.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.action; 2 | 3 | // TODO: Split instant and long actions 4 | public enum Action { 5 | LEFT_WALK, RIGHT_WALK, JUMP, SHOOT; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/shooter/net/packets/DisconnectPacket.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net.packets; 2 | 3 | public class DisconnectPacket extends ClientPacket { 4 | private static final long serialVersionUID = -3675889282016261921L; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/shooter/SinglePlayerRunner.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter; 2 | 3 | import src.main.shooter.gui.client.ClientMainFrame; 4 | import src.main.shooter.net.Server; 5 | 6 | public class SinglePlayerRunner { 7 | public static void main(final String[] args) { 8 | Server.main(args); 9 | ClientMainFrame.main(args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/shooter/net/packets/ClientPacket.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net.packets; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Used when the client wants to send a packet to the server. 7 | * 8 | * Stores only the actions--instant and long, of an entity. 9 | * (I feel like my grammar is incorrect.) 10 | */ 11 | public class ClientPacket implements Serializable { 12 | private static final long serialVersionUID = -8731243900388342502L; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/HorDirectionedEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | public interface HorDirectionedEntity { 4 | public enum HorDirection { 5 | LEFT(-1), RIGHT(1); 6 | 7 | private final int sign; 8 | 9 | private HorDirection(final int sign) { 10 | this.sign = sign; 11 | } 12 | 13 | public int getSign() { 14 | return sign; 15 | } 16 | } 17 | 18 | public HorDirection getHorDirection(); 19 | 20 | public void setHorDirection(HorDirection horDirection); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/GravitationalEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | import src.main.shooter.game.ServerGame.GameSettings; 4 | 5 | public interface GravitationalEntity extends KineticEntity { 6 | /** 7 | * Standardly, will return GameSettings.GLOBAL_GRAVITY 8 | * 9 | * @return Gravitational force in terms of dist/(tick sqrd). 10 | */ 11 | public default double getGravitationalForce() { 12 | return GameSettings.GLOBAL_GRAVITY; 13 | } 14 | 15 | public default void applyGravity() { 16 | shiftYVel(getGravitationalForce()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/PlatformEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | import src.main.shooter.game.ServerGame; 4 | import src.main.shooter.game.ServerGame.Entity; 5 | 6 | public class PlatformEntity extends Entity { 7 | private static final long serialVersionUID = 423302831329368937L; 8 | 9 | public PlatformEntity(final ServerGame game, final double width, final double height, final double x, 10 | final double y) { 11 | super(game, width, height, x, y); 12 | } 13 | 14 | @Override 15 | public void tick() { 16 | } 17 | 18 | @Override 19 | public void handleCollision(final Entity otherEntity) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/shooter/gui/client/DeathPanel.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.client; 2 | 3 | import java.awt.event.*; 4 | 5 | import javax.swing.JButton; 6 | import javax.swing.JLabel; 7 | import javax.swing.JPanel; 8 | 9 | public class DeathPanel extends JPanel { 10 | public DeathPanel() { 11 | add(new JLabel("You died.")); 12 | add(new JButton("Replay") { 13 | { 14 | addActionListener(new ActionListener() { 15 | @Override 16 | public void actionPerformed(final ActionEvent e) { 17 | ((ClientMainFrame) DeathPanel.this.getTopLevelAncestor()).setMenuPanel(); 18 | ; 19 | }; 20 | }); 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/shooter/gui/server/ServerRunningPanel.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.server; 2 | 3 | import java.net.InetAddress; 4 | import java.net.UnknownHostException; 5 | 6 | import javax.swing.JLabel; 7 | import javax.swing.JPanel; 8 | 9 | import src.main.shooter.net.Server; 10 | 11 | public class ServerRunningPanel extends JPanel { 12 | public ServerRunningPanel(final Server server) { 13 | try { 14 | add(new JLabel( 15 | "Running. Port: " + 16 | server.getServerSocket().getLocalPort() + 17 | ". IP address: " + 18 | InetAddress.getLocalHost().getHostAddress())); 19 | } catch (final UnknownHostException e) { 20 | e.printStackTrace(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/shooter/net/packets/ActionPacket.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net.packets; 2 | 3 | import src.main.shooter.game.ClientGame; 4 | import src.main.shooter.game.ServerGame.Entity; 5 | import src.main.shooter.game.action.ActionSet; 6 | 7 | public class ActionPacket extends ClientPacket { 8 | private static final long serialVersionUID = -710902470934092114L; 9 | public final ActionSet actionSet; 10 | 11 | @Deprecated 12 | public ActionPacket(final Entity entity) { 13 | actionSet = entity.getActionSet(); 14 | } 15 | 16 | public ActionPacket(final ClientGame game) { 17 | this.actionSet = game.getActionSet(); 18 | // this.actionSet = new ActionSet(); 19 | // this.actionSet.getLongActions().add(null); 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "Packet [actionSet=" + actionSet + "]"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/Vector2D.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | public class Vector2D { 4 | private double x, y; 5 | 6 | public Vector2D() { 7 | this(0, 0); 8 | } 9 | 10 | public Vector2D(final double x, final double y) { 11 | this.x = x; 12 | this.y = y; 13 | } 14 | 15 | public double getX() { 16 | return x; 17 | } 18 | 19 | public void setX(final double x) { 20 | this.x = x; 21 | } 22 | 23 | public double incX(final double dX) { 24 | return this.x += dX; 25 | } 26 | 27 | public double getY() { 28 | return y; 29 | } 30 | 31 | public void setY(final double y) { 32 | this.y = y; 33 | } 34 | 35 | public double incY(final double dY) { 36 | return this.y += dY; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "Vector2D [x=" + x + ", y=" + y + "]"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/TeamedPlayerEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | import src.main.shooter.game.ServerGame; 4 | import src.main.shooter.game.ServerGame.Entity; 5 | 6 | public class TeamedPlayerEntity extends PlayerEntity { 7 | public enum Team { 8 | RED, BLUE; 9 | } 10 | 11 | private final Team team; 12 | 13 | public TeamedPlayerEntity(final ServerGame game, final Team team, final double x, final double y, 14 | final HorDirection direction) { 15 | super(game, x, y, direction); 16 | this.team = team; 17 | } 18 | 19 | public Team getTeam() { 20 | return team; 21 | } 22 | 23 | @Override 24 | public void handleCollision(final Entity otherEntity) { 25 | if (otherEntity instanceof final BulletEntity bulletEntity) { 26 | if (bulletEntity.getShooter() instanceof final TeamedPlayerEntity shooter) { 27 | if (shooter.team == this.team) { 28 | return; 29 | } 30 | } 31 | } 32 | 33 | super.handleCollision(otherEntity); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/shooter/gui/client/ClientMenuPanel.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.client; 2 | 3 | import java.awt.event.ActionEvent; 4 | import java.awt.event.ActionListener; 5 | 6 | import javax.swing.JButton; 7 | import javax.swing.JPanel; 8 | import javax.swing.JTextField; 9 | 10 | import src.main.shooter.net.Server; 11 | 12 | public class ClientMenuPanel extends JPanel { 13 | private final JTextField ipAddress, portNumber; 14 | 15 | public ClientMenuPanel() { 16 | ipAddress = new JTextField("localhost"); 17 | add(ipAddress); 18 | 19 | portNumber = new JTextField("" + Server.DEFAULT_PORT_NUMBER); 20 | add(portNumber); 21 | 22 | add(new JButton("Join Game") { 23 | { 24 | addActionListener(new ActionListener() { 25 | @Override 26 | public void actionPerformed(final ActionEvent e) { 27 | ((ClientMainFrame) ClientMenuPanel.this.getTopLevelAncestor()).startGame(ipAddress.getText(), 28 | Integer.parseInt(portNumber.getText())); 29 | } 30 | }); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/shooter/game/action/ActionSet.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.action; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | 6 | import src.main.shooter.utils.ArraySet; 7 | 8 | /** 9 | * More about actions are in the {@code server} class, but instant actions are 10 | * done instantly and long actions are done every tick until it is told not to 11 | * be done. 12 | * 13 | * To clear actions or add or remove, do getInstantActions and directly 14 | * call functions on them such as add() or clear() or 15 | * 16 | * @see src.main.shooter.net.Server 17 | * @see java.util.ArrayList 18 | */ 19 | public class ActionSet implements Serializable { 20 | private static final long serialVersionUID = -4852037557772448218L; 21 | 22 | private final ArrayList instantActions; 23 | private final ArraySet longActions; 24 | 25 | public ActionSet() { 26 | instantActions = new ArrayList(); 27 | longActions = new ArraySet(); 28 | } 29 | 30 | public ArrayList getInstantActions() { 31 | return instantActions; 32 | } 33 | 34 | public ArraySet getLongActions() { 35 | return longActions; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "ActionSet [instantActions=" + instantActions + ", longActions=" + longActions + "]"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/shooter/gui/server/ServerMenuPanel.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.server; 2 | 3 | import java.awt.event.ActionEvent; 4 | import java.awt.event.ActionListener; 5 | 6 | import javax.swing.JButton; 7 | import javax.swing.JComboBox; 8 | import javax.swing.JPanel; 9 | import javax.swing.JTextField; 10 | 11 | import src.main.shooter.net.Server; 12 | 13 | public class ServerMenuPanel extends JPanel { 14 | private final JComboBox gameType; 15 | private final JTextField portNumber; 16 | 17 | public ServerMenuPanel() { 18 | final String[] gameTypes = { 19 | "Red vs Blue", 20 | "F4A" 21 | }; 22 | gameType = new JComboBox(gameTypes); 23 | add(gameType); 24 | 25 | portNumber = new JTextField("" + Server.DEFAULT_PORT_NUMBER); 26 | add(portNumber); 27 | 28 | add(new JButton("Start Server") { 29 | { 30 | addActionListener(new ActionListener() { 31 | @Override 32 | public void actionPerformed(final ActionEvent e) { 33 | ((ServerMainFrame) ServerMenuPanel.this.getTopLevelAncestor()) 34 | .startServer(Integer.parseInt(portNumber.getText()), 35 | gameTypes[gameType.getSelectedIndex()]); 36 | } 37 | }); 38 | } 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/PistolEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | import src.main.shooter.game.ServerGame.Entity; 4 | import src.main.shooter.game.entities.HorDirectionedEntity.HorDirection; 5 | 6 | public class PistolEntity extends Entity { 7 | private static final long serialVersionUID = 348408736704866955L; 8 | 9 | private final PlayerEntity owner; 10 | 11 | public PistolEntity(final PlayerEntity owner) { 12 | // the 3 and 2 are dimensions of the pistol. the division by 8 is because the 13 | // player is 16x8 pixels and considered 2x1 in the game 14 | super(owner.getGame(), 3. / 8., 2. / 8., Double.NaN, Double.NaN); 15 | 16 | this.owner = owner; 17 | } 18 | 19 | @Override 20 | public void tick() { 21 | } 22 | 23 | @Override 24 | public void handleCollision(final Entity otherEntity) { 25 | } 26 | 27 | @Override 28 | public double getX() { 29 | return getCenterX() - getWidth() / 2; 30 | } 31 | 32 | @Override 33 | public double getY() { 34 | return owner.getBottomY() + 1.25; 35 | } 36 | 37 | @Override 38 | public double getCenterX() { 39 | return owner.getCenterX() + owner.getHorDirection().getSign() * getXOffset(); 40 | } 41 | 42 | private double getXOffset() { 43 | return owner.getWidth() / 2. + this.getWidth() / 2. + 1. / 8.; 44 | } 45 | 46 | public HorDirection getHorDirection() { 47 | return owner.getHorDirection(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/KineticEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | /** 4 | * Something with position and velocity. 5 | */ 6 | public interface KineticEntity { 7 | public void setX(double newX); 8 | 9 | public void setY(double newY); 10 | 11 | public void setXVel(double newXVel); 12 | 13 | public void setYVel(double newYVel); 14 | 15 | public double getX(); 16 | 17 | public double getXVel(); 18 | 19 | public double getY(); 20 | 21 | public double getYVel(); 22 | 23 | /** 24 | * @param dX shift in x 25 | * @return new x 26 | */ 27 | public default double shiftX(final double dX) { 28 | setX(getX() + dX); 29 | return getX(); 30 | } 31 | 32 | /** 33 | * @param dXVel shift in xVel 34 | * @return new xVel 35 | */ 36 | public default double shiftXVel(final double dXVel) { 37 | setXVel(getXVel() + dXVel); 38 | return getXVel(); 39 | } 40 | 41 | /** 42 | * @param dY shift in y 43 | * @return new y 44 | */ 45 | public default double shiftY(final double dY) { 46 | setY(getY() + dY); 47 | return getY(); 48 | } 49 | 50 | /** 51 | * @param dYVel shift in yVel 52 | * @return new yVel 53 | */ 54 | public default double shiftYVel(final double dYVel) { 55 | setYVel(getYVel() + dYVel); 56 | return getYVel(); 57 | } 58 | 59 | public default void applyVelocity() { 60 | shiftX(getXVel()); 61 | shiftY(getYVel()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/shooter/game/ClientGame.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game; 2 | 3 | import java.util.TreeMap; 4 | 5 | import src.main.shooter.game.ServerGame.Entity; 6 | import src.main.shooter.game.action.ActionSet; 7 | import src.main.shooter.game.entities.PlayerEntity; 8 | import src.main.shooter.net.Client; 9 | 10 | public class ClientGame { 11 | private final int playerId; 12 | private final Client client; 13 | 14 | private final ActionSet actionSet; 15 | 16 | private TreeMap entities; 17 | 18 | public ClientGame(final Client client, final int clientId) { 19 | this.client = client; 20 | playerId = clientId; 21 | actionSet = new ActionSet(); 22 | } 23 | 24 | public ActionSet getActionSet() { 25 | return actionSet; 26 | } 27 | 28 | @Deprecated 29 | public PlayerEntity getPlayerEntity() { 30 | return (PlayerEntity) entities.get(playerId); 31 | } 32 | 33 | public void addEntity(final Entity entity) { 34 | entities.put(entity.getId(), entity); 35 | } 36 | 37 | public TreeMap getEntities() { 38 | return entities; 39 | } 40 | 41 | public void processEntityList(final TreeMap incomingEntityList) { 42 | entities = incomingEntityList; 43 | } 44 | 45 | public void tick() { 46 | // TODO: game logic 47 | if (!getEntities().containsKey(playerId)) { 48 | // TODO: find way to end all threads without exit() 49 | // make a method in client like client.handleDeath() or maybe simplify 50 | // client.disconnect() 51 | client.disconnect(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/shooter/gui/server/ServerMainFrame.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.server; 2 | 3 | import java.awt.event.WindowAdapter; 4 | import java.awt.event.WindowEvent; 5 | 6 | import javax.swing.JFrame; 7 | 8 | import src.main.shooter.game.ServerGame; 9 | import src.main.shooter.game.ServerTeamGame; 10 | import src.main.shooter.net.Server; 11 | 12 | public class ServerMainFrame extends JFrame { 13 | private Server server; 14 | 15 | public ServerMainFrame() { 16 | super("Server Manager"); 17 | 18 | add(new ServerMenuPanel()); 19 | 20 | addWindowListener(new WindowAdapter() { 21 | @Override 22 | public void windowClosing(final WindowEvent e) { 23 | if (server != null) { 24 | server.closeServer(); 25 | } 26 | System.exit(0); 27 | } 28 | }); 29 | 30 | pack(); 31 | setVisible(true); 32 | } 33 | 34 | public void startServer(final int port, final String gameType) { 35 | getContentPane().removeAll(); 36 | 37 | System.out.println("Starting server."); 38 | 39 | server = new Server(switch (gameType) { 40 | case ("F4A") -> new ServerGame(); 41 | case ("Red vs Blue") -> new ServerTeamGame(); 42 | default -> throw new IllegalArgumentException("Unexpected value: " + gameType); 43 | }, port); 44 | 45 | add(new ServerRunningPanel(server)); 46 | 47 | pack(); 48 | revalidate(); 49 | repaint(); 50 | 51 | server.run(); 52 | } 53 | 54 | public static void main(final String[] args) { 55 | new ServerMainFrame(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/shooter/game/ServerTeamGame.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game; 2 | 3 | import src.main.shooter.game.entities.TeamedPlayerEntity; 4 | import src.main.shooter.game.entities.HorDirectionedEntity.HorDirection; 5 | import src.main.shooter.game.entities.TeamedPlayerEntity.Team; 6 | import src.main.shooter.utils.ArraySet; 7 | 8 | public class ServerTeamGame extends ServerGame { 9 | private final ArraySet redTeamPlayers = new ArraySet<>(), blueTeamPlayers = new ArraySet<>(); 10 | 11 | public ServerTeamGame() { 12 | super(); 13 | 14 | System.out.println("Playing teams."); 15 | } 16 | 17 | @Override 18 | public int spawnPlayerEntity() { 19 | TeamedPlayerEntity playerEntity; 20 | if (redTeamPlayers.size() < blueTeamPlayers.size()) { 21 | playerEntity = new TeamedPlayerEntity(this, Team.RED, 1, 3, 22 | HorDirection.RIGHT); 23 | redTeamPlayers.add(playerEntity); 24 | System.out.println("New red team player."); 25 | } else { 26 | playerEntity = new TeamedPlayerEntity(this, Team.BLUE, 8, 3, 27 | HorDirection.LEFT); 28 | blueTeamPlayers.add(playerEntity); 29 | System.out.println("New blue team player."); 30 | } 31 | return playerEntity.getId(); 32 | } 33 | 34 | @Override 35 | public void removeEntity(final int id) { 36 | if (getEntities().get(id) instanceof final TeamedPlayerEntity teamedPlayerEntity) { 37 | if (teamedPlayerEntity.getTeam() == Team.RED) { 38 | redTeamPlayers.remove(teamedPlayerEntity); 39 | } else { 40 | blueTeamPlayers.remove(teamedPlayerEntity); 41 | } 42 | } 43 | super.removeEntity(id); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/shooter/utils/ArraySet.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.utils; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.Collection; 6 | import java.util.Iterator; 7 | import java.util.Set; 8 | 9 | /** 10 | * Just an arraylist, but it implements set so that it seems like a set. 11 | */ 12 | public class ArraySet implements Set, Serializable { 13 | private static final long serialVersionUID = -5207065003950887744L; 14 | private final ArrayList list = new ArrayList(); 15 | 16 | @Override 17 | public int size() { 18 | return list.size(); 19 | } 20 | 21 | @Override 22 | public boolean isEmpty() { 23 | return list.isEmpty(); 24 | } 25 | 26 | @Override 27 | public boolean contains(final Object o) { 28 | return list.contains(o); 29 | } 30 | 31 | @Override 32 | public Iterator iterator() { 33 | return list.iterator(); 34 | } 35 | 36 | @Override 37 | public Object[] toArray() { 38 | return list.toArray(); 39 | } 40 | 41 | @Override 42 | public T[] toArray(final T[] a) { 43 | return list.toArray(a); 44 | } 45 | 46 | @Override 47 | public boolean add(final E e) { 48 | return list.add(e); 49 | } 50 | 51 | @Override 52 | public boolean remove(final Object o) { 53 | return list.remove(o); 54 | } 55 | 56 | @Override 57 | public boolean containsAll(final Collection c) { 58 | return list.containsAll(c); 59 | } 60 | 61 | @Override 62 | public boolean addAll(final Collection c) { 63 | return list.addAll(c); 64 | } 65 | 66 | @Override 67 | public boolean retainAll(final Collection c) { 68 | return list.retainAll(c); 69 | } 70 | 71 | @Override 72 | public boolean removeAll(final Collection c) { 73 | return list.removeAll(c); 74 | } 75 | 76 | @Override 77 | public void clear() { 78 | list.clear(); 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | return list.toString(); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/shooter/gui/client/ClientMainFrame.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.client; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.event.WindowAdapter; 5 | import java.awt.event.WindowEvent; 6 | import java.io.IOException; 7 | import java.net.ConnectException; 8 | 9 | import javax.swing.JFrame; 10 | 11 | import src.main.shooter.net.Client; 12 | 13 | public class ClientMainFrame extends JFrame { 14 | private Client client; 15 | 16 | public ClientMainFrame() { 17 | super("Game"); 18 | 19 | setMenuPanel(); 20 | 21 | addWindowListener(new WindowAdapter() { 22 | @Override 23 | public void windowClosing(final WindowEvent e) { 24 | if (client != null) { 25 | client.disconnect(); 26 | } 27 | 28 | System.exit(0); 29 | } 30 | }); 31 | 32 | pack(); 33 | setVisible(true); 34 | } 35 | 36 | public void setMenuPanel() { 37 | getContentPane().removeAll(); 38 | 39 | add(new ClientMenuPanel()); 40 | 41 | pack(); 42 | revalidate(); 43 | repaint(); 44 | } 45 | 46 | public boolean startGame(final String ipAddress, final int port) { 47 | System.out.println("Starting game."); 48 | try { 49 | client = new Client(this, ipAddress, port); 50 | } catch (final ConnectException e) { 51 | System.out.println("Connection refused."); 52 | return false; 53 | } catch (final IOException e) { 54 | e.printStackTrace(); 55 | } 56 | 57 | getContentPane().removeAll(); 58 | 59 | final ClientGamePanel gamePanel = new ClientGamePanel(client.getGame()); 60 | 61 | add(gamePanel, BorderLayout.CENTER); 62 | 63 | gamePanel.requestFocusInWindow(); // must be after adding gamePanel to this 64 | 65 | pack(); 66 | revalidate(); 67 | repaint(); 68 | 69 | client.run(); 70 | return true; 71 | } 72 | 73 | public void handleDeath() { 74 | getContentPane().removeAll(); 75 | 76 | add(new DeathPanel()); 77 | 78 | pack(); 79 | revalidate(); 80 | repaint(); 81 | } 82 | 83 | public static void main(final String[] args) { 84 | new ClientMainFrame(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/chatroom/Server.java: -------------------------------------------------------------------------------- 1 | package src.main.chatroom; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | import java.net.Socket; 6 | import java.util.ArrayList; 7 | 8 | public class Server { 9 | public static final int port = 1234; 10 | 11 | public static void main(final String[] args) throws IOException { 12 | new Server(new ServerSocket(port)); 13 | } 14 | 15 | private final ServerSocket serverSocket; 16 | 17 | private ArrayList clientHandlers; 18 | 19 | public Server(final ServerSocket serverSocket) { 20 | this.serverSocket = serverSocket; 21 | startAcceptClientLoop(); 22 | } 23 | 24 | // sender client to server to every client 25 | public void broadcastMessage(final ClientHandler sender, final String message) { 26 | for (final ClientHandler clientHandler : clientHandlers) { 27 | clientHandler.recieveMessage(sender, message); 28 | } 29 | System.out.println("Message has been broadcasted."); 30 | } 31 | 32 | private void sendServerMessage(final String message) { 33 | for (final ClientHandler clientHandler : clientHandlers) { 34 | clientHandler.recieveServerMessage(message); 35 | } 36 | 37 | System.out.println("Server message has been broadcasted."); 38 | } 39 | 40 | private void startAcceptClientLoop() { 41 | System.out.println("Starting server."); 42 | clientHandlers = new ArrayList(); 43 | try { 44 | while (!serverSocket.isClosed()) { 45 | System.out.println("Waiting for new client."); 46 | final Socket socket = serverSocket.accept(); 47 | System.out.println("A new client has connected."); 48 | final ClientHandler clientHandler = new ClientHandler(this, socket); 49 | clientHandlers.add(clientHandler); 50 | System.out.println("A new client handler has been made and added."); 51 | new Thread(clientHandler).start(); 52 | System.out.println("A new client handler has been started."); 53 | sendServerMessage("A new user has joined."); 54 | } 55 | } catch (final IOException e) { 56 | System.out.println("There was an IOException."); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/chatroom/ClientHandler.java: -------------------------------------------------------------------------------- 1 | package src.main.chatroom; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStreamWriter; 8 | import java.net.Socket; 9 | 10 | public class ClientHandler implements Runnable { 11 | private String username; 12 | private Server server; 13 | private Socket socket; 14 | private BufferedWriter bufferedWriter; 15 | private BufferedReader bufferedReader; 16 | 17 | public ClientHandler(Server server, Socket socket) { 18 | this.server = server; 19 | this.socket = socket; 20 | try { 21 | this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 22 | this.bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 23 | } catch (IOException e) { 24 | e.printStackTrace(); 25 | } 26 | } 27 | 28 | @Override 29 | public void run() { 30 | setUsername(); 31 | startSendMessageLoop(); 32 | } 33 | 34 | // server to client 35 | public void recieveMessage(ClientHandler sender, String message) { 36 | if (sender == this) { 37 | return; 38 | } 39 | 40 | try { 41 | bufferedWriter.write(sender.username + ": " + message); 42 | bufferedWriter.newLine(); 43 | bufferedWriter.flush(); 44 | } catch (IOException e) { 45 | } 46 | } 47 | 48 | public void recieveServerMessage(String message) { 49 | try { 50 | bufferedWriter.write("SERVER: " + message); 51 | bufferedWriter.newLine(); 52 | bufferedWriter.flush(); 53 | } catch (IOException e) { 54 | } 55 | } 56 | 57 | private void setUsername() { 58 | try { 59 | username = bufferedReader.readLine(); 60 | } catch (IOException e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | 65 | // client to server 66 | private void startSendMessageLoop() { 67 | try { 68 | while (socket.isConnected()) { 69 | String message = bufferedReader.readLine(); 70 | server.broadcastMessage(ClientHandler.this, message); 71 | } 72 | } catch (IOException e) { 73 | e.printStackTrace(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/BulletEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | import src.main.shooter.game.ServerGame; 4 | import src.main.shooter.game.ServerGame.Entity; 5 | 6 | public class BulletEntity extends Entity implements HorDirectionedEntity { 7 | private static final long serialVersionUID = 2690256651740709424L; 8 | 9 | private final PlayerEntity shooter; 10 | private int age = 0; 11 | private final double horVel; 12 | 13 | public BulletEntity(final ServerGame game, final PlayerEntity shooter, final double x, final double y, 14 | final double horVel, 15 | final XAxisType xAxisType, final YAxisType yAxisType) { 16 | /* 17 | * TODO: add a rectangle class, this is becoming a problem 18 | */ 19 | super(game, 2. / 8., 1. / 8., x, y); 20 | 21 | this.shooter = shooter; 22 | 23 | switch (xAxisType) { 24 | case LEFT: 25 | setX(x); 26 | break; 27 | case RIGHT: 28 | setX(x - this.getWidth()); 29 | break; 30 | case CENTER: 31 | // idk 32 | break; 33 | } 34 | 35 | switch (yAxisType) { 36 | case BOTTOM: 37 | setY(y); 38 | break; 39 | case TOP: 40 | setY(y - this.getHeight()); 41 | break; 42 | case CENTER: 43 | // idk 44 | break; 45 | } 46 | 47 | age = 0; 48 | this.horVel = horVel; 49 | } 50 | 51 | @Override 52 | public void tick() { 53 | if (age >= ServerGame.GameSettings.BULLET_LIFESPAN) { 54 | getGame().removeEntity(getId()); 55 | return; 56 | } 57 | 58 | shiftX(horVel); 59 | 60 | age++; 61 | } 62 | 63 | @Override 64 | public void handleCollision(final Entity otherEntity) { 65 | // death should be handled enemy side 66 | if (otherEntity instanceof PlatformEntity) { 67 | getGame().removeEntity(getId()); 68 | } 69 | } 70 | 71 | @Override 72 | public HorDirection getHorDirection() { 73 | return horVel > 0 ? HorDirection.RIGHT : HorDirection.LEFT; 74 | } 75 | 76 | public PlayerEntity getShooter() { 77 | return shooter; 78 | } 79 | 80 | @Override 81 | public void setHorDirection(final HorDirection horDirection) { 82 | throw new UnsupportedOperationException("Unimplemented method 'setHorDirection'"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/chatroom/Client.java: -------------------------------------------------------------------------------- 1 | package src.main.chatroom; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStreamWriter; 8 | import java.net.Socket; 9 | import java.util.Scanner; 10 | 11 | public class Client { 12 | public static void main(final String[] args) { 13 | new Client(Server.port); 14 | } 15 | 16 | private Socket socket; 17 | private BufferedReader bufferedReader; 18 | private BufferedWriter bufferedWriter; 19 | 20 | public Client(final int port) { 21 | try { 22 | socket = new Socket("localhost", port); 23 | this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 24 | this.bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 25 | 26 | setUsername(); 27 | startReadMessageLoop(); 28 | startSendMessageLoop(); 29 | } catch (final IOException e) { 30 | e.printStackTrace(); 31 | } 32 | } 33 | 34 | private void setUsername() { 35 | System.out.print("Enter your username: "); 36 | try (Scanner scanner = new Scanner(System.in)) { 37 | bufferedWriter.write(scanner.nextLine()); 38 | bufferedWriter.newLine(); 39 | bufferedWriter.flush(); 40 | } catch (final IOException e) { 41 | e.printStackTrace(); 42 | } 43 | System.out.println("Sent username information."); 44 | } 45 | 46 | private void startSendMessageLoop() { 47 | try (Scanner scanner = new Scanner(System.in)) { 48 | while (socket.isConnected()) { 49 | bufferedWriter.write(scanner.nextLine()); 50 | bufferedWriter.newLine(); 51 | bufferedWriter.flush(); 52 | } 53 | System.out.println("Send message loop done."); 54 | } catch (final IOException e) { 55 | e.printStackTrace(); 56 | } 57 | // new Thread(new Runnable() { 58 | // @Override 59 | // public void run() { 60 | // } 61 | // }).start(); 62 | } 63 | 64 | private void startReadMessageLoop() { 65 | new Thread(new Runnable() { 66 | @Override 67 | public void run() { 68 | try { 69 | while (socket.isConnected()) { 70 | System.out.println(bufferedReader.readLine()); 71 | } 72 | System.out.println("Read message loop done."); 73 | } catch (final IOException e) { 74 | e.printStackTrace(); 75 | } 76 | } 77 | }).start(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/shooter/gui/client/ClientInputHandler.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.client; 2 | 3 | import java.awt.event.KeyEvent; 4 | import java.awt.event.KeyListener; 5 | import java.util.ArrayList; 6 | 7 | import src.main.shooter.game.ClientGame; 8 | import src.main.shooter.game.action.Action; 9 | import src.main.shooter.utils.ArraySet; 10 | 11 | public class ClientInputHandler implements KeyListener { 12 | /** 13 | * In mac and other OS, when a key is held for ~5 seconds, the OS counts it as 14 | * being rapidly pressed. This is to ensure that press only refers to when 15 | */ 16 | private final boolean[] previouslyPressed; 17 | private final ClientGame game; 18 | 19 | public ClientInputHandler(final ClientGame game) { 20 | this.game = game; 21 | previouslyPressed = new boolean[0xFFFF]; 22 | for (int i = 0; i < previouslyPressed.length; i++) { 23 | previouslyPressed[i] = false; 24 | } 25 | } 26 | 27 | @Override 28 | public void keyTyped(final KeyEvent e) { 29 | } 30 | 31 | @Override 32 | public void keyPressed(final KeyEvent e) { 33 | final int keyCode = e.getKeyCode(); 34 | 35 | if (previouslyPressed[keyCode]) { 36 | // means that this is due to rapid press in the OS 37 | return; 38 | } 39 | 40 | final ArraySet longActions = game.getActionSet().getLongActions(); 41 | final ArrayList instantActions = game.getActionSet().getInstantActions(); 42 | switch (keyCode) { 43 | case KeyEvent.VK_A: { 44 | longActions.add(Action.LEFT_WALK); 45 | break; 46 | } 47 | 48 | case KeyEvent.VK_D: { 49 | longActions.add(Action.RIGHT_WALK); 50 | break; 51 | } 52 | 53 | case KeyEvent.VK_SPACE: { 54 | instantActions.add(Action.JUMP); 55 | break; 56 | } 57 | 58 | case KeyEvent.VK_ENTER: { 59 | instantActions.add(Action.SHOOT); 60 | break; 61 | } 62 | 63 | default: 64 | break; 65 | } 66 | previouslyPressed[keyCode] = true; 67 | } 68 | 69 | @Override 70 | public void keyReleased(final KeyEvent e) { 71 | final int keyCode = e.getKeyCode(); 72 | 73 | final ArraySet longActions = game.getActionSet().getLongActions(); 74 | 75 | switch (keyCode) { 76 | case KeyEvent.VK_A: { 77 | longActions.remove(Action.LEFT_WALK); 78 | break; 79 | } 80 | 81 | case KeyEvent.VK_D: { 82 | longActions.remove(Action.RIGHT_WALK); 83 | break; 84 | } 85 | 86 | default: 87 | break; 88 | } 89 | previouslyPressed[keyCode] = false; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/shooter/net/ClientHandler.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net; 2 | 3 | import java.io.IOException; 4 | import java.io.ObjectInputStream; 5 | import java.io.ObjectOutputStream; 6 | import java.net.Socket; 7 | import java.util.TreeMap; 8 | 9 | import src.main.shooter.game.ServerGame.Entity; 10 | import src.main.shooter.net.packets.ClientPacket; 11 | 12 | public class ClientHandler implements Runnable { 13 | private boolean isRunning; 14 | private final int entityId; 15 | private final Server server; 16 | private final Socket socket; 17 | private ObjectOutputStream outputStream; 18 | private ObjectInputStream inputStream; 19 | 20 | public int getEntityId() { 21 | return entityId; 22 | } 23 | 24 | public ClientHandler(final Server server, final Socket socket, final int id) { 25 | this.server = server; 26 | this.socket = socket; 27 | this.entityId = id; 28 | 29 | try { 30 | this.outputStream = new ObjectOutputStream(socket.getOutputStream()); 31 | this.inputStream = new ObjectInputStream(socket.getInputStream()); 32 | } catch (final IOException e) { 33 | e.printStackTrace(); 34 | } 35 | 36 | initialClientCommunication(); 37 | } 38 | 39 | private void initialClientCommunication() { 40 | try { 41 | outputStream.writeInt(entityId); 42 | server.sendUpdates(this); 43 | } catch (final IOException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | @Override 49 | public void run() { 50 | isRunning = true; 51 | startRecieveMessageLoop(); 52 | } 53 | 54 | // client to server 55 | private void startRecieveMessageLoop() { 56 | while (isRunning) { 57 | try { 58 | final ClientPacket packet = (ClientPacket) inputStream.readObject(); 59 | server.processPacket(this, packet); 60 | } catch (final IOException e) { 61 | e.printStackTrace(); 62 | } catch (final ClassNotFoundException e) { 63 | e.printStackTrace(); 64 | } 65 | } 66 | } 67 | 68 | // server to client 69 | public void sendUpdate(final TreeMap update) { 70 | if (!isRunning) { 71 | return; 72 | } 73 | 74 | try { 75 | outputStream.writeObject(update); 76 | outputStream.reset(); 77 | } catch (final IOException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | 82 | public void disconnect() { 83 | System.out.println("A client has disconnected."); 84 | 85 | isRunning = false; 86 | 87 | // close everything 88 | try { 89 | socket.close(); 90 | inputStream.close(); 91 | outputStream.close(); 92 | } catch (final IOException e) { 93 | e.printStackTrace(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/shooter/net/Client.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net; 2 | 3 | import java.io.IOException; 4 | import java.io.ObjectInputStream; 5 | import java.io.ObjectOutputStream; 6 | import java.net.Socket; 7 | import java.net.SocketException; 8 | import java.util.TreeMap; 9 | 10 | import src.main.shooter.game.ClientGame; 11 | import src.main.shooter.game.ServerGame.Entity; 12 | import src.main.shooter.gui.client.ClientMainFrame; 13 | import src.main.shooter.net.packets.ActionPacket; 14 | import src.main.shooter.net.packets.ClientPacket; 15 | import src.main.shooter.net.packets.DisconnectPacket; 16 | 17 | public class Client implements Runnable { 18 | private boolean isRunning; 19 | private final Socket socket; 20 | private final ObjectInputStream inputStream; 21 | private final ObjectOutputStream outputStream; 22 | private ClientGame game; 23 | private final ClientMainFrame mainFrame; 24 | 25 | public ClientGame getGame() { 26 | return game; 27 | } 28 | 29 | public Client(final ClientMainFrame mainFrame, final String ipAddress, final int port) throws IOException { 30 | socket = new Socket(ipAddress, port); 31 | inputStream = new ObjectInputStream(socket.getInputStream()); 32 | outputStream = new ObjectOutputStream(socket.getOutputStream()); 33 | 34 | this.mainFrame = mainFrame; 35 | 36 | initialServerCommunication(); 37 | } 38 | 39 | @SuppressWarnings("unchecked") 40 | private void initialServerCommunication() { 41 | try { 42 | final int clientId = inputStream.readInt(); 43 | game = new ClientGame(this, clientId); 44 | 45 | game.processEntityList(((TreeMap) inputStream.readObject())); 46 | 47 | System.out.println("Finished initial server communication."); 48 | } catch (final IOException e) { 49 | e.printStackTrace(); 50 | } catch (final ClassNotFoundException e) { 51 | e.printStackTrace(); 52 | } 53 | } 54 | 55 | @Override 56 | public void run() { 57 | isRunning = true; 58 | new Thread(() -> startReadAndWriteLoop()).start(); 59 | new Thread(() -> startGameloop()).start(); 60 | } 61 | 62 | @SuppressWarnings("unchecked") // if type is bad, then it should throw an error anyways 63 | private void startReadAndWriteLoop() { 64 | while (isRunning) { 65 | try { 66 | // read 67 | game.processEntityList(((TreeMap) inputStream.readObject())); 68 | 69 | // write 70 | sendPacket(new ActionPacket(game)); 71 | game.getActionSet().getInstantActions().clear(); 72 | } catch (final SocketException e) { 73 | break; 74 | } catch (final IOException e) { 75 | e.printStackTrace(); 76 | } catch (final ClassNotFoundException e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | } 81 | 82 | private void startGameloop() { 83 | while (isRunning) { 84 | game.tick(); 85 | mainFrame.repaint(); 86 | } 87 | } 88 | 89 | public void sendPacket(final ClientPacket packet) { 90 | try { 91 | outputStream.writeObject(packet); 92 | outputStream.reset(); 93 | } catch (final IOException e) { 94 | e.printStackTrace(); 95 | } 96 | } 97 | 98 | public void disconnect() { 99 | System.out.println("Disconnecting from server."); 100 | 101 | isRunning = false; 102 | 103 | // tell server we disconnected 104 | sendPacket(new DisconnectPacket()); 105 | 106 | // close everything 107 | try { 108 | socket.close(); 109 | inputStream.close(); 110 | outputStream.close(); 111 | } catch (final IOException e) { 112 | e.printStackTrace(); 113 | } 114 | 115 | mainFrame.handleDeath(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/shooter/game/entities/PlayerEntity.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game.entities; 2 | 3 | import src.main.shooter.game.ServerGame; 4 | import src.main.shooter.game.ServerGame.Entity; 5 | import src.main.shooter.game.ServerGame.GameSettings; 6 | import src.main.shooter.game.action.Action; 7 | 8 | public class PlayerEntity extends Entity implements HorDirectionedEntity, GravitationalEntity { 9 | private static final long serialVersionUID = -3022640676588904126L; 10 | 11 | private final PistolEntity pistol; 12 | 13 | private HorDirection horDirection; 14 | private double xVel, yVel; 15 | 16 | public PlayerEntity(final ServerGame game, final double x, final double y, 17 | final HorDirection direction) { 18 | super(game, 1, 2, x, y); 19 | 20 | this.horDirection = direction; 21 | 22 | xVel = 0; 23 | yVel = 0; 24 | 25 | this.pistol = new PistolEntity(this); 26 | } 27 | 28 | @Override 29 | public double getYVel() { 30 | return yVel; 31 | } 32 | 33 | @Override 34 | public void setYVel(final double yVel) { 35 | this.yVel = yVel; 36 | } 37 | 38 | @Override 39 | public double getXVel() { 40 | return xVel; 41 | } 42 | 43 | @Override 44 | public void setXVel(final double xVel) { 45 | this.xVel = xVel; 46 | } 47 | 48 | @Override 49 | public HorDirection getHorDirection() { 50 | return horDirection; 51 | } 52 | 53 | @Override 54 | public void setHorDirection(final HorDirection horDirection) { 55 | this.horDirection = horDirection; 56 | } 57 | 58 | @Override 59 | public void tick() { 60 | for (final Action action : getActionSet().getInstantActions()) { 61 | switch (action) { 62 | case JUMP: { 63 | if (getYVel() != 0) { 64 | break; 65 | } 66 | shiftYVel(GameSettings.JUMP_VEL); 67 | break; 68 | } 69 | 70 | case SHOOT: { 71 | if (pistol.getHorDirection() == HorDirection.LEFT) { 72 | new BulletEntity(getGame(), this, pistol.getLeftX(), pistol.getTopY(), 73 | -GameSettings.BULLET_SPEED, 74 | XAxisType.LEFT, YAxisType.TOP); 75 | } else if (pistol.getHorDirection() == HorDirection.RIGHT) { 76 | new BulletEntity(getGame(), this, pistol.getRightX(), pistol.getTopY(), 77 | GameSettings.BULLET_SPEED, 78 | XAxisType.RIGHT, YAxisType.TOP); 79 | } else { 80 | ServerGame.getLogger().warning("Unknown direction \"" + pistol.getHorDirection() + "\"."); 81 | } 82 | break; 83 | } 84 | 85 | default: 86 | ServerGame.getLogger().warning("Unknown action \"" + action + "\" in instant actions."); 87 | break; 88 | } 89 | } 90 | getActionSet().getInstantActions().clear(); 91 | 92 | for (final Action action : getActionSet().getLongActions()) { 93 | switch (action) { 94 | case LEFT_WALK: { 95 | setHorDirection(HorDirection.LEFT); 96 | shiftX(-GameSettings.WALK_SPEED); 97 | break; 98 | } 99 | 100 | case RIGHT_WALK: { 101 | setHorDirection(HorDirection.RIGHT); 102 | shiftX(GameSettings.WALK_SPEED); 103 | break; 104 | } 105 | 106 | default: 107 | ServerGame.getLogger().warning("Unknown action \"" + action + "\" in long actions."); 108 | break; 109 | } 110 | } 111 | 112 | // physics 113 | applyGravity(); 114 | applyVelocity(); 115 | } 116 | 117 | @Override 118 | public void handleCollision(final Entity otherEntity) { 119 | if (otherEntity instanceof PlatformEntity) { 120 | final Vector2D collisionNormal = getCollisionNormal(otherEntity); 121 | 122 | if (collisionNormal.getX() > 0) { 123 | // set right of this to the left of other 124 | this.setX(otherEntity.getX() - this.getWidth()); 125 | this.setXVel(0); 126 | } else if (collisionNormal.getX() < 0) { 127 | // set left of this to the right of other 128 | this.setX(otherEntity.getX() + otherEntity.getWidth()); 129 | this.setXVel(0); 130 | } 131 | 132 | if (collisionNormal.getY() > 0) { 133 | // set top of this to the bottom of other 134 | this.setY(otherEntity.getY() - this.getHeight()); 135 | this.setYVel(0); 136 | } else if (collisionNormal.getY() < 0) { 137 | // set bottom of this to the top of other 138 | this.setY(otherEntity.getY() + otherEntity.getHeight()); 139 | this.setYVel(0); 140 | } 141 | } else if (otherEntity instanceof BulletEntity) { 142 | die(); 143 | } 144 | } 145 | 146 | private void die() { // might cause concurrent mod issues 147 | getGame().removeEntity(getId()); 148 | } 149 | } 150 | 151 | // TODO: add tree like structure to entities so that players can officially own 152 | // guns -------------------------------------------------------------------------------- /src/main/shooter/net/Server.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.net; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | import java.net.Socket; 6 | import java.util.ArrayList; 7 | 8 | import src.main.shooter.game.ServerGame; 9 | import src.main.shooter.net.packets.ActionPacket; 10 | import src.main.shooter.net.packets.DisconnectPacket; 11 | import src.main.shooter.net.packets.ClientPacket; 12 | 13 | /** 14 | * Can either make server tell every single client every single entity's 15 | * position every frame, or can make server tell every client the new position 16 | * for an entity every time it changes position. 17 | * 18 | * The second is probably better in ways such as efficiency, but I will do the 19 | * first for now. Also it kind of makes more sense with UDP. 20 | * 21 | * 22 | * 23 | * As for the chain of communication, this is how a key press would work: GUI 24 | * tells client that key was pressed. Client tells server that key was pressed. 25 | * On the server side, the server tells it's game that the key was pressed. 26 | * 27 | * Every game tick, the game does game logic. The server tells client what 28 | * changed. Client updates gui based off of the change. 29 | * 30 | * 31 | * 32 | * Also, the roles for server and game seem very similar. For example, it makes 33 | * sense to put the gameloop in the game class, but I put it in the server 34 | * class. This is because the game should only contain the game logic. In a real 35 | * time multiplayer game, it does make sense to give the game class 36 | * responsibility for the gameloop. Both make sense, but I decided that the game 37 | * should be like it's own world and so the role that real time has on the game 38 | * should be kept outside of the game class. For example, take tictactoe. The 39 | * game class would contain the board and everything inside the boards. To it, 40 | * it doesn't matter how many seconds have passed (unless it is a timed game). 41 | * It only cares what spots are x or o or blank. It doesn't matter if the Red 42 | * player is an ai or that there has been 3 games before the current game. 43 | * Obviousily, if the game was a timed game, then real time would matter. But, I 44 | * don't want this game to care about real time. For now, time and the tick 45 | * count will be in the server class. However, if there is a feature such as the 46 | * game getting harder as the tick increases, then the tick count might be moved 47 | * to the game class. 48 | * 49 | * 50 | * 51 | * Speaking of real time, we must also consider that the client's ticks will be 52 | * offset from the server's. This is made worse with multiple clients. I can 53 | * either make actions discrete or continuous. It will be best to make 54 | * everything discrete, but since I am currently using UDP, that isn't really a 55 | * good idea. To explain better, take the example of walking. If I made this 56 | * discrete, I would say walking is just repeatedly teleporting a certain 57 | * meters every tick. In terms of continuity, I could say that walking is 58 | * smoothly traveling at a rate of a certain meters per tick. I can also ignore 59 | * actions and say there is only state. By state, I don't mean states like 60 | * walking but position. Regardless of walking or teleporting or standing, the 61 | * client could always tell the server the player's position. This would not be 62 | * good for animations, but it makes the most sense with UDP. Another way is 63 | * saying that the player is doing the action of walking and is at a certain 64 | * position. A lot of these methods seem to make sense with walking, but what 65 | * about instantanious actions like throwing an egg? Sense this only happens for 66 | * one frame, we can't risk missing this information. With walking, it is 67 | * forgivable to miss a walking action since in the next frame, we can just get 68 | * the new position. However, with instant actions, we should use TCP and we 69 | * can't just do the same thing like telling the player's position because if we 70 | * do that with inventory, we'd say there was an egg and then the egg wasn't in 71 | * the player's inventory. The game is able to deduce that the egg was thrown, 72 | * but how would it know the angle of the throw and the type of throw? It is not 73 | * ideal to make the client give information about the egg since the client 74 | * should only give information about the player. So, we can make it send 75 | * information about the throw. It is tempting to use UDP for speed, but I 76 | * conclude that TCP is the better choice. This is also better for chatting. 77 | * With TCP, I can make the client tell the server about every button press, and 78 | * the server takes care of every input, but that seems unnecessary. I will 79 | * assume that all actions are instant and discrete. Walking is instantly 80 | * teleportating every tick. All actions are committed at a whole number time 81 | * like t = 10 but not t = 10.5, but there can still be order with actions 82 | * committed at the same tick. For example, even if switching items and using 83 | * selected item were committed at t=10, one must be done before the other. 84 | */ 85 | public class Server implements Runnable { 86 | private final int TICKS_PER_SECOND = 20; 87 | private final int MILLISECONDS_PER_TICK = 1000000000 / TICKS_PER_SECOND; 88 | 89 | public final static int DEFAULT_PORT_NUMBER = 1234; 90 | 91 | private final ServerGame game; 92 | private ServerSocket serverSocket; 93 | private final ArrayList clientHandlers; 94 | 95 | public Server(final ServerGame game, final int port) { 96 | this.game = game; 97 | try { 98 | this.serverSocket = new ServerSocket(port); 99 | } catch (final IOException e) { 100 | e.printStackTrace(); 101 | } 102 | clientHandlers = new ArrayList(); 103 | } 104 | 105 | @Override 106 | public void run() { 107 | new Thread(() -> startAcceptClientsLoop()).start(); 108 | new Thread(() -> startGameloop()).start(); 109 | } 110 | 111 | private void startAcceptClientsLoop() { 112 | System.out.println("Accepting Clients."); 113 | while (true) { 114 | System.out.println("Waiting for new client."); 115 | try { 116 | final Socket socket = serverSocket.accept(); 117 | System.out.println("A new client has connected."); 118 | final ClientHandler clientHandler = new ClientHandler(this, socket, game.spawnPlayerEntity()); 119 | clientHandlers.add(clientHandler); 120 | new Thread(clientHandler).start(); 121 | } catch (final IOException e) { 122 | e.printStackTrace(); 123 | } 124 | } 125 | } 126 | 127 | private void startGameloop() { 128 | long lastTickTime = System.nanoTime(); 129 | 130 | while (true) { 131 | final long whenShouldNextTickRun = lastTickTime + MILLISECONDS_PER_TICK; 132 | if (System.nanoTime() < whenShouldNextTickRun) { 133 | continue; 134 | } 135 | 136 | game.tick(); 137 | 138 | sendUpdatesToAll(); 139 | 140 | lastTickTime = System.nanoTime(); 141 | } 142 | } 143 | 144 | public void processPacket(final ClientHandler clientHandler, final ClientPacket packet) { 145 | if (packet instanceof final ActionPacket actionPacket) { 146 | game.updateActionSet(clientHandler.getEntityId(), actionPacket.actionSet); 147 | } else if (packet instanceof final DisconnectPacket disconnectPacket) { 148 | clientHandler.disconnect(); 149 | game.removeEntity(clientHandler.getEntityId()); 150 | clientHandlers.remove(clientHandler); 151 | } 152 | } 153 | 154 | // server to all client 155 | private void sendUpdatesToAll() { 156 | for (final ClientHandler clientHandler : clientHandlers) { 157 | sendUpdates(clientHandler); 158 | } 159 | } 160 | 161 | // server to one client 162 | public void sendUpdates(final ClientHandler clientHandler) { 163 | clientHandler.sendUpdate(game.getEntities()); 164 | } 165 | 166 | public void closeServer() { 167 | // TODO: save state or something 168 | 169 | try { 170 | serverSocket.close(); 171 | } catch (final IOException e) { 172 | e.printStackTrace(); 173 | } 174 | } 175 | 176 | public ServerSocket getServerSocket() { 177 | return serverSocket; 178 | } 179 | 180 | public static void main(final String[] args) { 181 | new Server(new ServerGame(), Server.DEFAULT_PORT_NUMBER).run(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/shooter/game/ServerGame.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.game; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.io.Serializable; 6 | import java.util.Scanner; 7 | import java.util.TreeMap; 8 | import java.util.logging.Logger; 9 | 10 | import src.main.shooter.game.action.ActionSet; 11 | import src.main.shooter.game.entities.HorDirectionedEntity.HorDirection; 12 | import src.main.shooter.game.entities.PlatformEntity; 13 | import src.main.shooter.game.entities.PlayerEntity; 14 | import src.main.shooter.game.entities.Vector2D; 15 | 16 | public class ServerGame { 17 | /** 18 | * Static so that the implicit reference to outer class isn't serialized. 19 | * 20 | * Should I create a rectangle class? 21 | */ 22 | public static abstract class Entity implements Serializable { 23 | private static final long serialVersionUID = -1816334362202070857L; 24 | 25 | private transient final ServerGame game; 26 | private final int id; 27 | 28 | private final double width, height; 29 | 30 | private double x, y; // bottom left corner, not center 31 | private ActionSet actionSet; 32 | 33 | public Entity(final ServerGame game, final double width, final double height, final double x, 34 | final double y) { 35 | this.game = game; 36 | this.id = this.game.getSmallestAvailableId(); 37 | this.width = width; 38 | this.height = height; 39 | this.x = x; 40 | this.y = y; 41 | 42 | actionSet = new ActionSet(); 43 | 44 | game.addEntity(this); 45 | } 46 | 47 | public ServerGame getGame() { 48 | return game; 49 | } 50 | 51 | public ActionSet getActionSet() { 52 | return actionSet; 53 | } 54 | 55 | public final int getId() { 56 | return id; 57 | } 58 | 59 | public double getWidth() { 60 | return width; 61 | } 62 | 63 | public double getHeight() { 64 | return height; 65 | } 66 | 67 | public double shiftX(final double shiftFactor) { 68 | return x += shiftFactor; 69 | } 70 | 71 | public double shiftY(final double shiftFactor) { 72 | return y += shiftFactor; 73 | } 74 | 75 | /** 76 | * @return the X coordinate value of the left bottom point of the entity. 77 | */ 78 | public double getX() { 79 | return x; 80 | } 81 | 82 | /** 83 | * @return the Y coordinate value of the left bottom point of the entity. 84 | */ 85 | public double getY() { 86 | return y; 87 | } 88 | 89 | public double getLeftX() { 90 | return getX(); 91 | } 92 | 93 | public double getBottomY() { 94 | return getY(); 95 | } 96 | 97 | public double getRightX() { 98 | return getLeftX() + getWidth(); 99 | } 100 | 101 | public double getTopY() { 102 | return getBottomY() + getHeight(); 103 | } 104 | 105 | public double getCenterX() { 106 | return getLeftX() + getWidth() / 2; 107 | } 108 | 109 | public double getCenterY() { 110 | return getBottomY() + getHeight() / 2; 111 | } 112 | 113 | public void setX(final double x) { 114 | this.x = x; 115 | } 116 | 117 | public void setY(final double y) { 118 | this.y = y; 119 | } 120 | 121 | public void setActionSet(final ActionSet actionSet) { 122 | this.actionSet = actionSet; 123 | } 124 | 125 | /** 126 | * I'd rather have the game just calculate everything, because of things like 127 | * gravity and collisions. However, this is the standard way apparently. 128 | */ 129 | public abstract void tick(); 130 | 131 | public abstract void handleCollision(Entity otherEntity); 132 | 133 | public boolean isColliding(final Entity otherEntity) { 134 | // Uses AABB collision 135 | return getX() < otherEntity.getX() + otherEntity.getWidth() && getX() + getWidth() > otherEntity.getX() 136 | && getY() < otherEntity.getY() + otherEntity.getHeight() 137 | && getY() + getHeight() > otherEntity.getY() 138 | && this != otherEntity; 139 | } 140 | 141 | public Vector2D getCollisionNormal(final Entity otherEntity) { 142 | // Edge case: collision with a very thin object and jumping on the edge of 143 | // platform 144 | final double xOverlap = Math.min(this.getX() + this.getWidth(), otherEntity.getX() + otherEntity.getWidth()) 145 | - Math.max(this.getX(), otherEntity.getX()); 146 | final double yOverlap = Math.min(this.getY() + this.getHeight(), 147 | otherEntity.getY() + otherEntity.getHeight()) 148 | - Math.max(this.getY(), otherEntity.getY()); 149 | 150 | if (xOverlap > yOverlap) { // smaller matters more 151 | return new Vector2D(0, Math.signum(otherEntity.getY() - this.getY())); 152 | } else if (xOverlap < yOverlap) { 153 | return new Vector2D(Math.signum(otherEntity.getX() - this.getX()), 0); 154 | } else { 155 | return new Vector2D(Math.signum(otherEntity.getX() - this.getY()), 156 | Math.signum(otherEntity.getY() - this.getY())); 157 | } 158 | } 159 | } 160 | 161 | public class GameSettings { 162 | public static final double GLOBAL_GRAVITY = -0.05; 163 | 164 | public static final double WALK_SPEED = 0.125; 165 | public static final double JUMP_VEL = 0.5; 166 | 167 | public static final double BULLET_SPEED = 0.25; 168 | public static final int BULLET_LIFESPAN = 20; 169 | } 170 | 171 | private static final Logger logger = Logger.getLogger("Server"); 172 | 173 | public static Logger getLogger() { 174 | return logger; 175 | } 176 | 177 | private int smallestAvailableId = 0; // Use a UUID generator? 178 | private final TreeMap entities; 179 | 180 | public ServerGame() { 181 | entities = new TreeMap(); 182 | 183 | init(); 184 | } 185 | 186 | public TreeMap getEntities() { 187 | // recommended to iterate through a copy of this 188 | return entities; 189 | } 190 | 191 | public void updateActionSet(final int id, final ActionSet actionSet) { 192 | entities.get(id).setActionSet(actionSet); 193 | } 194 | 195 | public void removeEntity(final int id) { 196 | // TODO: would have some saving in here 197 | entities.remove(id); 198 | } 199 | 200 | public void tick() { 201 | /** 202 | * to avoid concurrent modification exceptions 203 | */ 204 | final TreeMap entitiesCopy = new TreeMap<>(entities); 205 | for (final Entity entity : entitiesCopy.values()) { 206 | entity.tick(); 207 | } 208 | 209 | // collisions 210 | for (final Entity entity1 : entitiesCopy.values()) { 211 | for (final Entity entity2 : entitiesCopy.values()) { 212 | if (entity1.isColliding(entity2)) { 213 | entity1.handleCollision(entity2); 214 | } 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * Creates a new player entity, adds the entity to the game, and returns the ID 221 | * of the new entity. 222 | * 223 | * @return New entity's id. 224 | */ 225 | public int spawnPlayerEntity() { 226 | final PlayerEntity player = new PlayerEntity(this, 4.5, 5, HorDirection.LEFT); 227 | return player.getId(); 228 | } 229 | 230 | /** 231 | * ! WARNING: DO NOT CALL THIS MORE THAN NEEDED, IT CHANGES THE ID NUMBER 232 | * 233 | *

234 | * 235 | * It's not too big a deal, worst that could happen is that there is an unused 236 | * ID number. However, understand that this does assume that you will be using 237 | * the ID. 238 | * 239 | * @return the smallest available id 240 | */ 241 | private int getSmallestAvailableId() { 242 | return smallestAvailableId++; 243 | } 244 | 245 | private void init() { 246 | // load world platforms 247 | try (Scanner platforms = new Scanner(new File("src/res/standard-map-platform-rectangles.csv"))) { 248 | while (platforms.hasNextLine()) { 249 | final String[] dimensionsString = platforms.nextLine().split(","); 250 | new PlatformEntity(this, 251 | Double.parseDouble(dimensionsString[0]), 252 | Double.parseDouble(dimensionsString[1]), 253 | Double.parseDouble(dimensionsString[2]), 254 | Double.parseDouble(dimensionsString[3])); 255 | } 256 | } catch (final FileNotFoundException e) { 257 | e.printStackTrace(); 258 | } 259 | } 260 | 261 | private void addEntity(final Entity entity) { 262 | entities.put(entity.getId(), entity); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/shooter/gui/client/ClientGamePanel.java: -------------------------------------------------------------------------------- 1 | package src.main.shooter.gui.client; 2 | 3 | import java.awt.Color; 4 | import java.awt.Dimension; 5 | import java.awt.Graphics; 6 | import java.awt.Graphics2D; 7 | import java.awt.geom.AffineTransform; 8 | import java.awt.image.AffineTransformOp; 9 | import java.awt.image.BufferedImage; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.util.logging.Logger; 13 | 14 | import javax.imageio.ImageIO; 15 | import javax.swing.JPanel; 16 | 17 | import src.main.shooter.game.ClientGame; 18 | import src.main.shooter.game.ServerGame.Entity; 19 | import src.main.shooter.game.entities.BulletEntity; 20 | import src.main.shooter.game.entities.PistolEntity; 21 | import src.main.shooter.game.entities.PlatformEntity; 22 | import src.main.shooter.game.entities.PlayerEntity; 23 | import src.main.shooter.game.entities.TeamedPlayerEntity; 24 | 25 | public class ClientGamePanel extends JPanel { 26 | private static final Logger logger = Logger.getLogger(ClientGamePanel.class.getName()); 27 | 28 | private final double[][] gameViewRanges = new double[][] { { 0, 10 }, { 0, 10 } }; // {xRange, yRange} 29 | private final ClientGame game; 30 | private final Sprites sprites = new Sprites(); 31 | 32 | private static class Sprites { 33 | private final BufferedImage rightPlayerSprite, 34 | leftPlayerSprite, 35 | rightBluePlayerSprite, 36 | leftBluePlayerSprite, 37 | rightPistolSprite, 38 | leftPistolSprite, 39 | rightBulletSprite, 40 | leftBulletSprite; 41 | 42 | private Sprites() { 43 | try { 44 | rightPlayerSprite = ImageIO.read(new File("src/res/Right-Facing-Red-Shooter.png")); 45 | leftPlayerSprite = getReflectedImage(rightPlayerSprite); 46 | 47 | rightBluePlayerSprite = ImageIO.read(new File("src/res/Right-Facing-Blue-Shooter.png")); 48 | leftBluePlayerSprite = getReflectedImage(rightBluePlayerSprite); 49 | 50 | rightPistolSprite = ImageIO.read(new File("src/res/Right-Facing-Pistol.png")); 51 | leftPistolSprite = getReflectedImage(rightPistolSprite); 52 | 53 | rightBulletSprite = ImageIO.read(new File("src/res/Right-Facing-Bullet.png")); 54 | leftBulletSprite = getReflectedImage(rightBulletSprite); 55 | } catch (final IOException e) { 56 | throw new RuntimeException(e); 57 | } 58 | } 59 | 60 | private static BufferedImage getReflectedImage(final BufferedImage originalImage) { 61 | // flip right player sprite to get left player sprite 62 | final AffineTransform transform = AffineTransform.getScaleInstance(-1, 1); 63 | transform.translate(-originalImage.getWidth(), 0); 64 | final AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); 65 | return op.filter(originalImage, null); 66 | } 67 | } 68 | 69 | public ClientGamePanel(final ClientGame game) { 70 | this.game = game; 71 | 72 | addKeyListener(new ClientInputHandler(game)); 73 | 74 | setFocusable(true); 75 | } 76 | 77 | @Override 78 | public Dimension getPreferredSize() { 79 | return new Dimension(720, 720); 80 | } 81 | 82 | @Override 83 | public Dimension getMinimumSize() { 84 | return getPreferredSize(); 85 | } 86 | 87 | @Override 88 | public void paint(final Graphics g) { 89 | // System.out.println("Starting to paint."); 90 | final Graphics2D graphics2d = (Graphics2D) g; 91 | 92 | drawDebugGrid(graphics2d); 93 | 94 | for (final Entity entity : game.getEntities().values()) { 95 | // System.out.println("Entity " + entity.getId() + 96 | // ": [x=" + entity.getX() + ", y=" + entity.getY() + ", w=" 97 | // + entity.getWidth() + ", h=" + entity.getHeight() + "]"); 98 | 99 | // because swing doesn't work with negative sizes 100 | final int x1 = remapXCoords(entity.getX()), y1 = remapYCoords(entity.getY()), 101 | x2 = x1 + rescaleWidth(entity.getWidth()), y2 = y1 + rescaleHeight(entity.getHeight()); 102 | 103 | final int minX = Math.min(x1, x2), minY = Math.min(y1, y2), maxX = Math.max(x1, x2), 104 | maxY = Math.max(y1, y2); 105 | final int width = maxX - minX, height = maxY - minY; 106 | 107 | final BufferedImage sprite; 108 | 109 | if (entity instanceof final PlayerEntity playerEntity) { 110 | if (playerEntity instanceof final TeamedPlayerEntity teamedPlayerEntity) { 111 | sprite = switch (teamedPlayerEntity.getTeam()) { 112 | case RED -> switch (playerEntity.getHorDirection()) { 113 | case LEFT -> sprites.leftPlayerSprite; 114 | case RIGHT -> sprites.rightPlayerSprite; 115 | }; 116 | case BLUE -> switch (playerEntity.getHorDirection()) { 117 | case LEFT -> sprites.leftBluePlayerSprite; 118 | case RIGHT -> sprites.rightBluePlayerSprite; 119 | }; 120 | }; 121 | } else { 122 | sprite = switch (playerEntity.getHorDirection()) { 123 | case LEFT -> sprites.leftPlayerSprite; 124 | case RIGHT -> sprites.rightPlayerSprite; 125 | }; 126 | } 127 | } else if (entity instanceof final PistolEntity pistolEntity) { 128 | sprite = switch (pistolEntity.getHorDirection()) { 129 | case LEFT -> sprites.leftPistolSprite; 130 | case RIGHT -> sprites.rightPistolSprite; 131 | }; 132 | } else if (entity instanceof final BulletEntity bulletEntity) { 133 | sprite = switch (bulletEntity.getHorDirection()) { 134 | case LEFT -> sprites.leftBulletSprite; 135 | case RIGHT -> sprites.rightBulletSprite; 136 | }; 137 | } else if (entity instanceof final PlatformEntity platformEntity) { 138 | // TODO: create actual sprite 139 | sprite = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); 140 | final Graphics2D spriteGraphics2d = sprite.createGraphics(); 141 | spriteGraphics2d.setColor(Color.BLUE); 142 | spriteGraphics2d.drawRect(0, 0, 1, 1); 143 | spriteGraphics2d.dispose(); 144 | } else { 145 | // create a purple square 146 | sprite = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); 147 | final Graphics2D spriteGraphics2d = sprite.createGraphics(); 148 | spriteGraphics2d.setColor(Color.MAGENTA); 149 | spriteGraphics2d.drawRect(0, 0, 1, 1); 150 | spriteGraphics2d.dispose(); 151 | 152 | logger.warning("Entity of unknown type"); 153 | } 154 | graphics2d.drawImage(sprite, minX, minY, width, height, null); 155 | } 156 | } 157 | 158 | private void drawDebugGrid(final Graphics2D graphics2d) { 159 | graphics2d.setColor(Color.BLACK); 160 | 161 | // vert 162 | for (int gameX = (int) gameViewRanges[0][0] - 1; gameX < gameViewRanges[0][1] + 1; gameX++) { 163 | graphics2d.drawLine(remapXCoords(gameX), 0, remapXCoords(gameX), getHeight()); 164 | } 165 | 166 | // hor 167 | for (int gameY = (int) gameViewRanges[1][0] - 1; gameY < gameViewRanges[1][1] + 1; gameY++) { 168 | graphics2d.drawLine(0, remapYCoords(gameY), getWidth(), remapYCoords(gameY)); 169 | } 170 | } 171 | 172 | private int remapXCoords(final double gameX) { 173 | return remap(gameX, gameViewRanges[0][0], gameViewRanges[0][1], 0, getWidth()); 174 | } 175 | 176 | private int remapYCoords(final double gameY) { 177 | return remap(gameY, gameViewRanges[1][0], gameViewRanges[1][1], getHeight(), 0); 178 | } 179 | 180 | private int remap(final double initialPoint, final double initialBottom, final double initialTop, 181 | final double newBottom, final double newTop) { 182 | 183 | // ratio of [length between point and bottom]:[total initial length] 184 | final double ratio = (initialPoint - initialBottom) / (initialTop - initialBottom); 185 | 186 | // distance between the new point and new bottom 187 | final double newDist = (newTop - newBottom) * ratio; 188 | 189 | final double newPoint = newBottom + newDist; 190 | return (int) Math.round(newPoint); // round because if cast to int across 0, it is not good 191 | } 192 | 193 | private int rescaleWidth(final double gameWidth) { 194 | return rescale(gameWidth, gameViewRanges[0][1] - gameViewRanges[0][0], getWidth()); 195 | } 196 | 197 | private int rescaleHeight(final double gameHeight) { 198 | return rescale(gameHeight, gameViewRanges[1][1] - gameViewRanges[1][0], -getHeight()); 199 | } 200 | 201 | private int rescale(final double initialLength, final double initialRange, final double newRange) { 202 | 203 | // ratio of [initial length]:[initial range] 204 | final double ratio = initialLength / initialRange; 205 | 206 | final double newLength = newRange * ratio; 207 | return (int) Math.round(newLength); 208 | } 209 | } --------------------------------------------------------------------------------