diff --git a/cs654/final-proj/BlobWars.java b/cs654/final-proj/BlobWars.java index ac22133..145f012 100644 --- a/cs654/final-proj/BlobWars.java +++ b/cs654/final-proj/BlobWars.java @@ -1,29 +1,174 @@ import java.awt.*; +import java.awt.event.*; import javax.swing.*; +import javax.swing.event.*; import javax.swing.border.*; +import java.net.*; +import java.io.*; public class BlobWars extends JFrame { - public static void main(String[] args) - { - BlobWars jt = new BlobWars(); - } + private BlobWarsWorld m_world; + private BlobWarsPanel m_panel; + private JTextField m_nameField; + private JTextField m_serverField; + private JButton m_connectButton; + private EventHandler m_handler; + private javax.swing.Timer m_timer; + private Socket m_socket; + private BufferedWriter m_writer; public BlobWars() { super("Josh's Blob Wars Game for CS654"); - setSize(400, 500); + m_world = new BlobWarsWorld(); + /* TODO: REMOVE */ + m_world.addPlayer("Alice"); + m_world.getPlayer("Alice").health = 0.8; + m_world.addPlayer("Bob the Bad"); + m_world.getPlayer("Bob the Bad").health = 0.6; + m_world.addPlayer("Jo"); + m_world.getPlayer("Jo").health = 0.4; + m_world.addPlayer("Triskaidekaphobia"); + m_world.getPlayer("Triskaidekaphobia").health = 0.2; + /* END TODO */ + + m_handler = new EventHandler(); + m_panel = new BlobWarsPanel(this, m_world); setDefaultCloseOperation(EXIT_ON_CLOSE); -// setResizable(false); + setResizable(false); getContentPane().add(new JPanel() {{ setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); add(new JPanel() {{ setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); - add(new JLabel("hi!")); + add(new JLabel("Player name: ")); + add(m_nameField = new JTextField()); }}); - add(new BlobWarsPanel()); + add(new JPanel() {{ + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + add(new JLabel("Server: ")); + add(m_serverField = new JTextField()); + m_serverField.addKeyListener(m_handler); + add(m_connectButton = new JButton("Connect")); + m_connectButton.addActionListener(m_handler); + }}); +// add(new Box.Filler(new Dimension(0, 0), +// new Dimension(0, 10000), +// new Dimension(10000, 10000))); +// add(new JPanel() {{ +// setLayout(new BorderLayout()); + add(m_panel); + m_panel.addKeyListener(m_handler); +// }}); }}); + setSize(getPreferredSize()); setVisible(true); } + + public String getPlayerName() + { + return m_nameField.getText(); + } + + private void doConnect() + { + try { + m_socket = new Socket(m_serverField.getText(), BlobWarsServer.PORT); + m_writer = new BufferedWriter( + new OutputStreamWriter(m_socket.getOutputStream())); + m_writer.write("NAME:" + m_nameField.getText() + "\n"); + } catch (Exception e) { + JOptionPane.showMessageDialog(null, + "Error connecting: " + e.getMessage()); + return; + } + (new Thread(new ClientHandler())).start(); + m_connectButton.setEnabled(false); + m_nameField.setEnabled(false); + m_serverField.setEnabled(false); + m_panel.requestFocusInWindow(); + m_timer = new javax.swing.Timer(50, m_handler); + } + + private class ClientHandler implements Runnable + { + public void run() + { + BufferedReader br; + try { + br = new BufferedReader( + new InputStreamReader(m_socket.getInputStream())); + } catch (Exception e) { + return; + } + + /* main client loop */ + for (;;) + { + String line = ""; + try { + line = br.readLine(); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println("GOT LINE: " + line); + } + } + } + + private class EventHandler implements ActionListener, KeyListener + { + public void actionPerformed(ActionEvent e) + { + if (e.getSource() == m_connectButton) + { + doConnect(); + } + } + + public void keyPressed(KeyEvent e) + { + if (e.getSource() == m_panel) + { + try { + switch (e.getKeyCode()) + { + case KeyEvent.VK_UP: + m_writer.write("UP\n"); + break; + case KeyEvent.VK_DOWN: + m_writer.write("DOWN\n"); + break; + case KeyEvent.VK_LEFT: + m_writer.write("LEFT\n"); + break; + case KeyEvent.VK_RIGHT: + m_writer.write("RIGHT\n"); + break; + } + } catch (Exception ex) {} + } + else if (e.getSource() == m_serverField) + { + if (e.getKeyCode() == KeyEvent.VK_ENTER) + { + doConnect(); + } + } + } + + public void keyReleased(KeyEvent e) + { + } + + public void keyTyped(KeyEvent e) + { + } + } + + public static void main(String[] args) + { + new BlobWars(); + } } diff --git a/cs654/final-proj/BlobWarsPanel.java b/cs654/final-proj/BlobWarsPanel.java index 6d81f5d..7e80487 100644 --- a/cs654/final-proj/BlobWarsPanel.java +++ b/cs654/final-proj/BlobWarsPanel.java @@ -2,21 +2,107 @@ import java.awt.*; import javax.swing.*; import javax.swing.border.*; +import java.util.*; public class BlobWarsPanel extends JPanel { - public BlobWarsPanel() + private BlobWarsWorld m_world; + private BlobWars m_gui; + + public BlobWarsPanel(BlobWars gui, BlobWarsWorld world) { - Dimension sz = new Dimension(400, 400); + Dimension sz = new Dimension(500, 500); setMinimumSize(sz); setPreferredSize(sz); setMaximumSize(sz); + m_world = world; + m_gui = gui; } public void paint(Graphics g) { -// System.out.println("Width: " + getWidth() + " Height: " + getHeight()); - g.setColor(Color.RED); - g.fillRect(0, 0, 40, 20); + Graphics2D g2d = (Graphics2D) g; + + /* draw background */ + g2d.setColor(Color.BLACK); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + /* draw players */ + for (Player p : m_world.getPlayers().values()) + { + drawPlayer(p, g); + } + } + + private void drawPlayer(Player p, Graphics g) + { + double xscl = p.x * getWidth(); + double yscl = p.y * getHeight(); + double rxscl = p.radius * getWidth(); + double ryscl = p.radius * getHeight(); + + /* draw the blob circle */ + g.setColor(p.name.equals(m_gui.getPlayerName()) + ? new Color(0.7f, 1.0f, 1.0f) + : Color.CYAN); + g.fillOval((int) (xscl - rxscl), + (int) (getHeight() - (yscl + ryscl)), + (int) (2 * rxscl), + (int) (2 * ryscl)); + g.setColor(Color.BLUE); + g.drawOval((int) (xscl - rxscl), + (int) (getHeight() - (yscl + ryscl)), + (int) (2 * rxscl), + (int) (2 * ryscl)); + + /* draw the health bar */ + Color hcol; + if (p.health >= 0.9) + hcol = Color.GREEN; + else if (p.health >= 0.5) + hcol = new Color((float) ((0.9 - p.health) / 0.4), 1.0f, 0.0f); + else if (p.health >= 0.1) + hcol = new Color(1.0f, (float) ((p.health - 0.1) / 0.4), 0.0f); + else + hcol = Color.RED; + int hbarwidth = (int) (rxscl * 1.4); + int hbarheight = (int) (ryscl * 0.2); + int hbarx = (int) (xscl - hbarwidth/2); + int hbary = (int) (getHeight() - (p.r < Math.PI + ? (yscl - 3*hbarheight/2) + : (yscl + 5*hbarheight/2))); + g.setColor(hcol); + g.fillRect(hbarx, hbary, (int) (hbarwidth * p.health), hbarheight); + g.setColor(Color.BLUE); + g.drawRect(hbarx, hbary, hbarwidth, hbarheight); + + /* draw the shooter triangle */ + int[] s_pointsx = new int[3]; + int[] s_pointsy = new int[3]; + s_pointsx[0] = (int) (xscl + rxscl * Math.cos(p.r)); + s_pointsy[0] = getHeight() - + (int) (yscl + ryscl * Math.sin(p.r)); + s_pointsx[1] = (int) (xscl + rxscl * 0.3 * Math.cos(p.r + Math.PI / 2)); + s_pointsy[1] = getHeight() - + (int) (yscl + ryscl * 0.3 * Math.sin(p.r + Math.PI / 2)); + s_pointsx[2] = (int) (xscl + rxscl * 0.3 * Math.cos(p.r - Math.PI / 2)); + s_pointsy[2] = getHeight() - + (int) (yscl + ryscl * 0.3 * Math.sin(p.r - Math.PI / 2)); + g.setColor(Color.WHITE); + g.fillPolygon(s_pointsx, s_pointsy, 3); + g.setColor(Color.BLACK); + g.drawPolygon(s_pointsx, s_pointsy, 3); + + /* draw the player's name */ + char[] chars = new char[p.name.length()]; + p.name.getChars(0, p.name.length(), chars, 0); + int width = g.getFontMetrics().charsWidth(chars, 0, p.name.length()); + int height = g.getFontMetrics().getHeight(); + g.setColor(Color.WHITE); + g.drawString(p.name, + (int) (xscl - width/2), + getHeight() - 4 - (p.y >= 0.5 + ? ((int) (yscl - ryscl - height)) + : ((int) (yscl + ryscl)) )); } } diff --git a/cs654/final-proj/BlobWarsServer.java b/cs654/final-proj/BlobWarsServer.java index ed5623c..1ce5a90 100644 --- a/cs654/final-proj/BlobWarsServer.java +++ b/cs654/final-proj/BlobWarsServer.java @@ -1,20 +1,52 @@ +import java.io.*; +import java.util.*; import java.net.*; -public class BlobWarsServer extends Thread +public class BlobWarsServer { public static final int PORT = 38491; private ServerSocket m_socket; + private BlobWarsWorld m_world; + private Vector m_clientUpdates; + private HashMap m_socketToPlayerName; + private HashMap m_lastPlayers; + private HashMap m_lastShots; - public void run() + public static void main(String[] args) { + BlobWarsServer bws = new BlobWarsServer(); + bws.mainloop(); + } + + public BlobWarsServer() + { + m_world = new BlobWarsWorld(); + m_clientUpdates = new Vector(); + m_socketToPlayerName = new HashMap(); try { m_socket = new ServerSocket(PORT); } catch (Exception e) { + e.printStackTrace(); return; } + } + private class ClientUpdate + { + public Socket socket; + public String message; + } + + public void mainloop() + { + if (m_socket == null) + return; + + new Thread(new ServerGameLoop()).start(); + + /* listen loop to accept new connections */ for (;;) { Socket client; @@ -22,6 +54,7 @@ public class BlobWarsServer extends Thread client = m_socket.accept(); } catch (Exception e) { System.out.println("Socket error!"); + e.printStackTrace(); break; } @@ -31,6 +64,127 @@ public class BlobWarsServer extends Thread } } + private void processClientUpdate(ClientUpdate cu) + { + StringTokenizer st = new StringTokenizer(cu.message, ":"); + Vector tokens = new Vector(); + while (st.hasMoreTokens()) + { + tokens.add(st.nextToken()); + } + String[] arr = tokens.toArray(new String[1]); + if (arr.length < 2) + return; + if (arr[0].equals("NAME")) + { + m_world.addPlayer(arr[1]); + m_socketToPlayerName.put(cu.socket, arr[1]); + return; + } + /* the rest of the commands all require a player to be registered */ + if (!m_socketToPlayerName.containsKey(cu.socket)) + return; + String playerName = m_socketToPlayerName.get(cu.socket); + if (arr[0].equals("SHOOT")) { + m_world.shoot(playerName); + } else if (arr[0].equals("UP")) { + m_world.moveUp(playerName); + } else if (arr[0].equals("DOWN")) { + m_world.moveDown(playerName); + } else if (arr[0].equals("LEFT")) { + m_world.moveLeft(playerName); + } else if (arr[0].equals("RIGHT")) { + m_world.moveRight(playerName); + } + } + + private class ServerGameLoop implements Runnable + { + public void run() + { + for (;;) + { + try { + Thread.sleep(45); + } catch (InterruptedException ie) { + System.out.println("ServerGameLoop: interrupted!"); + return; + } + + /* apply changes from clients */ + synchronized (m_clientUpdates) + { + for (ClientUpdate cu : m_clientUpdates) + { + processClientUpdate(cu); + } + m_clientUpdates.clear(); + } + + m_world.step(); + + /* push updates to clients */ + HashMap pushPlayers = + new HashMap(); + HashMap pushShots = + new HashMap(); + if (m_lastPlayers == null) + pushPlayers.putAll(m_world.getPlayers()); + else + { + HashMap players = m_world.getPlayers(); + for (String name : players.keySet()) + { + if (!m_lastPlayers.containsKey(name)) + pushPlayers.put(name, players.get(name)); + else if (!players.get(name).equals( + m_lastPlayers.get(name))) + pushPlayers.put(name, players.get(name)); + } + } + if (m_lastShots == null) + pushShots.putAll(m_world.getShots()); + else + { + HashMap shots = m_world.getShots(); + for (Integer shotid : shots.keySet()) + { + if (!m_lastShots.containsKey(shotid)) + pushShots.put(shotid, shots.get(shotid)); + else if (!shots.get(shotid).equals( + m_lastShots.get(shotid))) + pushShots.put(shotid, shots.get(shotid)); + } + } + + /* write updated info to each client */ + String sendLine = ""; + for (Player p : pushPlayers.values()) + { + sendLine += "PLAYER:" + p + "\n"; + } + for (Shot s : pushShots.values()) + { + sendLine += "SHOT:" + s + "\n"; + } + synchronized (m_socketToPlayerName) + { + for (Socket client : m_socketToPlayerName.keySet()) + { + try { + BufferedWriter br = new BufferedWriter( + new OutputStreamWriter( + client.getOutputStream())); + br.write(sendLine, 0, sendLine.length()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + } + private class ClientHandler implements Runnable { private Socket m_socket; @@ -38,11 +192,54 @@ public class BlobWarsServer extends Thread public ClientHandler(Socket socket) { m_socket = socket; - System.out.println("Incoming connection from " + m_socket.getInetAddress().getAddress()); + System.out.println("Incoming connection from " + + m_socket.getInetAddress().getHostAddress()); } public void run() { + BufferedReader br; + try { + br = new BufferedReader( + new InputStreamReader(m_socket.getInputStream())); + } catch (IOException ioe) { + ioe.printStackTrace(); + return; + } + + for (;;) + { + String line; + try { + line = br.readLine(); + } catch (IOException ioe) { + // TODO: remove this line to not print stack trace for + // a client that disconnects, and remove socket from + // HashMap after for() loop + ioe.printStackTrace(); + break; + } + System.out.println("SERVER GOT LINE: " + line); + if (line.equals("QUIT")) + { + synchronized (m_socketToPlayerName) + { + if (m_socketToPlayerName.containsKey(m_socket)) + m_socketToPlayerName.remove(m_socket); + try { + m_socket.close(); + } catch (Exception e) {} + } + break; + } + ClientUpdate cu = new ClientUpdate(); + cu.socket = m_socket; + cu.message = line; + synchronized (m_clientUpdates) + { + m_clientUpdates.add(cu); + } + } } } } diff --git a/cs654/final-proj/BlobWarsWorld.java b/cs654/final-proj/BlobWarsWorld.java new file mode 100644 index 0000000..b90f37f --- /dev/null +++ b/cs654/final-proj/BlobWarsWorld.java @@ -0,0 +1,206 @@ + +import java.util.*; + +public class BlobWarsWorld +{ + public static final double SHOT_SPEED = 0.3; + public static final double PLAYER_SPEED = 0.2; + public static final double PLAYER_SPIN_SPEED = Math.PI / 2; + + private HashMap m_players; + private HashMap m_shots; + private long m_lastStepTime; + private Integer m_shotID; + + public BlobWarsWorld() + { + m_players = new HashMap(); + m_shots = new HashMap(); + } + + public boolean playerExists(String name) + { + return m_players.containsKey(name); + } + + public Player getPlayer(String name) + { + return m_players.get(name); + } + + public boolean addPlayer(String name) + { + if (playerExists(name)) + return false; + Player p = new Player(name); + m_players.put(name, p); + do + { + p.x = (1.0 - 2 * Player.DEFAULT_RADIUS) * Math.random() + + Player.DEFAULT_RADIUS; + p.y = (1.0 - 2 * Player.DEFAULT_RADIUS) * Math.random() + + Player.DEFAULT_RADIUS; + } while (collidesWith(p).size() != 0); + + return true; + } + + public void shoot(String playerName) + { + if (!m_players.containsKey(playerName)) + return; + /* TODO: check validity of shot, create Shot object */ + Player p = m_players.get(playerName); + long curtime = (new Date()).getTime(); + if ( ((curtime - p.lastShotTime) / 1000.0) + < Player.SHOT_DELAY) + { + /* calculate X & Y coordinates of shot */ + double x = p.x + Math.cos(p.r) * (p.radius + Shot.DEFAULT_RADIUS); + double y = p.y + Math.sin(p.r) * (p.radius + Shot.DEFAULT_RADIUS); + m_shotID++; + Shot s = new Shot(m_shotID, curtime, x, y); + s.dx = Math.cos(p.r) * SHOT_SPEED; + s.dy = Math.sin(p.r) * SHOT_SPEED; + m_shots.put(m_shotID, s); + } + } + + public void moveUp(String playerName) + { + if (!m_players.containsKey(playerName)) + return; + Player p = m_players.get(playerName); + p.dx = Math.cos(p.r) * PLAYER_SPEED; + p.dy = Math.cos(p.r) * PLAYER_SPEED; + } + + public void moveDown(String playerName) + { + if (!m_players.containsKey(playerName)) + return; + Player p = m_players.get(playerName); + p.dx = 0; + p.dy = 0; + } + + public void moveLeft(String playerName) + { + if (!m_players.containsKey(playerName)) + return; + Player p = m_players.get(playerName); + p.dr = PLAYER_SPIN_SPEED; + } + + public void moveRight(String playerName) + { + if (!m_players.containsKey(playerName)) + return; + Player p = m_players.get(playerName); + p.dr = -PLAYER_SPIN_SPEED; + } + + public Vector collidesWith(GameItem gi) + { + Vector collideList = new Vector(); + for (Player p : m_players.values()) + { + if (p != gi && p.health > 0.0 && collidesWith(gi, p)) + collideList.add(p); + } + for (Shot s : m_shots.values()) + { + if (s != gi && collidesWith(gi, s)) + collideList.add(s); + } + return collideList; + } + + public boolean collidesWith(GameItem i1, GameItem i2) + { + double dx = i2.x - i1.x; + double dy = i2.y - i1.y; + double d2 = dx*dx + dy*dy; + double r = i1.radius + i2.radius; + double r2 = r*r; + return (d2 < r2); + } + + public HashMap getPlayers() + { + return m_players; + } + + public HashMap getShots() + { + return m_shots; + } + + public void step() + { + long curtime = (new Date()).getTime(); + double elapsed = (curtime - m_lastStepTime) / 1000.0; + if (m_lastStepTime == 0) + elapsed = 0.001; + for (Player p : m_players.values()) + { + p.x += p.dx * elapsed; + p.y += p.dy * elapsed; + p.r += p.dr * elapsed; + if (p.x < 0.0) p.x += 1.0; + if (p.x > 1.0) p.x -= 1.0; + if (p.y < 0.0) p.y += 1.0; + if (p.y > 1.0) p.y -= 1.0; + if (p.r < 0.0) p.r += Math.PI*2; + if (p.r > Math.PI*2) p.r -= Math.PI*2; + } + Vector shotIDs = new Vector(); + shotIDs.addAll(m_shots.keySet()); + for (Integer i : shotIDs) + { + Shot s = m_shots.get(i); + if (((curtime - s.createTime) / 1000.0) >= Shot.SHOT_DURATION) + { + m_shots.remove(i); + i++; + } + else + { + s.x += s.dx * elapsed; + s.y += s.dy * elapsed; + } + } + + /* calculate damage against players */ + for (Player p : m_players.values()) + { + Vector objs = collidesWith(p); + for (GameItem gi : objs) + { + if (gi instanceof Player) + { + p.health -= Player.COLLIDE_DAMAGE; + } + if (gi instanceof Shot) + { + p.health -= Shot.SHOT_DAMAGE; + Integer ifound = null; + for (Integer i : m_shots.keySet()) + { + if (m_shots.get(i) == gi) + { + ifound = i; + break; + } + } + if (ifound != null) + m_shots.remove(ifound); + } + if (p.health < 0.0) + p.health = 0.0; + } + } + m_lastStepTime = curtime; + } +} + diff --git a/cs654/final-proj/GameItem.java b/cs654/final-proj/GameItem.java new file mode 100644 index 0000000..e90573a --- /dev/null +++ b/cs654/final-proj/GameItem.java @@ -0,0 +1,9 @@ + +public class GameItem +{ + public double radius; + public double x; + public double y; + public double dx; + public double dy; +} diff --git a/cs654/final-proj/Player.java b/cs654/final-proj/Player.java new file mode 100644 index 0000000..3168ae1 --- /dev/null +++ b/cs654/final-proj/Player.java @@ -0,0 +1,41 @@ + +public class Player extends GameItem +{ + public static final double DEFAULT_RADIUS = 0.06; + public static final double COLLIDE_DAMAGE = 0.002; + public static final double SHOT_DELAY = 4.0; + + public String name; + public double health; + public double r; + public double dr; + public long lastShotTime; + + public Player(String name) + { + this.name = name; + this.radius = DEFAULT_RADIUS; + this.health = 1.0; + this.r = Math.random() * Math.PI * 2; + this.dr = 0; + this.dx = 0; + this.dy = 0; + } + + public boolean equals(Player other) + { + return this.name.equals(other.name) && + this.radius == other.radius && + this.health == other.health && + this.r == other.r && + this.dr == other.dr && + this.dx == other.dx && + this.dy == other.dy; + } + + public String toString() + { + return String.format("%s:%.3f:%.3f:%.3f:%.3f:%.3f:%.3f:%.3f:%.3f", + name, radius, health, x, y, r, dr, dx, dy); + } +} diff --git a/cs654/final-proj/Shot.java b/cs654/final-proj/Shot.java new file mode 100644 index 0000000..e72ee66 --- /dev/null +++ b/cs654/final-proj/Shot.java @@ -0,0 +1,37 @@ + +public class Shot extends GameItem +{ + public static final double DEFAULT_RADIUS = 0.03; + public static final double SHOT_DURATION = 3.0; + public static final double SHOT_DAMAGE = 0.27; + + public long createTime; + public Integer id; + + public Shot(Integer id, long createTime, double x, double y) + { + this.id = id; + this.createTime = createTime; + this.radius = DEFAULT_RADIUS; + this.x = x; + this.y = y; + this.dx = 0; + this.dy = 0; + } + + public boolean equals(Shot other) + { + return this.createTime == other.createTime && + this.radius == other.radius && + this.x == other.x && + this.y == other.y && + this.dx == other.dx && + this.dy == other.dy; + } + + public String toString() + { + return String.format("%d:%.3f:%.3f:%.3f:%.3f:%.3f", + id, radius, x, y, dx, dy); + } +} diff --git a/cs654/final-proj/make.bat b/cs654/final-proj/make.bat new file mode 100755 index 0000000..8a6b2ff --- /dev/null +++ b/cs654/final-proj/make.bat @@ -0,0 +1 @@ +javac *.java diff --git a/cs654/final-proj/run.bat b/cs654/final-proj/run.bat new file mode 100755 index 0000000..37effed --- /dev/null +++ b/cs654/final-proj/run.bat @@ -0,0 +1 @@ +java BlobWars diff --git a/cs654/final-proj/runserver.bat b/cs654/final-proj/runserver.bat new file mode 100755 index 0000000..d27a342 --- /dev/null +++ b/cs654/final-proj/runserver.bat @@ -0,0 +1 @@ +java BlobWarsServer