1

我正在使用套接字在 java 中制作多人蛇游戏。所有的传输都是通过服务器到所有连接的客户端完成的。相同的代码还没有完全完成,但它完成了移动蛇的基本工作,并在特定客户吃它的食物时增加分数。

我从服务器端生成食物坐标的随机数并将其转发给所有客户端。如果客户端按下某个键,则计算请求的移动并将移动方向发送到服务器,然后服务器将移动中继到所有客户端(包括发送它的客户端),并且只有在收到移动信息时客户端才会进行更改到移动的蛇。因此,每次移动都会通过网络进行跟踪,并且客户端本身不会做出移动决定,直到它收到,比如客户端“player1”要求移动。

我面临的问题是,即使有两个玩家,在蛇移动一点后坐标似乎也会有所不同。

我可以对我的代码应用哪些可能的补救措施,以消除蛇位置之间的明显滞后?

这是客户端代码:

package mycode;

import java.awt.Point;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Map;

import javax.swing.JOptionPane;

public class ConnectionManager implements Runnable {
    Socket socket;
    boolean start = false;
    DataInputStream in;
    DataOutputStream out;
    Map<String, Snake> map;

    ConnectionManager(String name, String IP, Map<String, Snake> m) {
        this.map = m;
        try {
            socket = new Socket(IP, 9977);
            in = new DataInputStream(new BufferedInputStream(
                    socket.getInputStream()));
            out = new DataOutputStream(new BufferedOutputStream(
                    socket.getOutputStream()));
            out.writeUTF(name);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
            JOptionPane.showMessageDialog(null, "Could Not Find Server",
                    "ERROR", JOptionPane.ERROR_MESSAGE);
            System.exit(0);
        }
    }

    void populateMap() {
        try {
            String name = in.readUTF();
            System.out.println("Name received: " + name);
            if (name.equals("start_game_9977")) {
                start = true;
                System.out.println("Game Started");
                return;
            } else if (name.equals("food_coord")) {
                Game.foodx = in.readInt();
                Game.foody = in.readInt();
                return;
            }
            map.put(name, new Snake(5));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    boolean start() {
        return start;
    }

    void increaseSnakeLength(String thisname){
        Snake temp = map.get(thisname);
        Point temp1=new Point(0,0);
        temp.length++;
        switch (temp.move) {
        case DOWN:
             temp1= new Point(temp.p[temp.length - 2].x,
                    temp.p[temp.length - 2].y+6);
             break;
        case LEFT:
            temp1= new Point(temp.p[temp.length - 2].x-6,
                    temp.p[temp.length - 2].y);
            break;
        case RIGHT:
            temp1= new Point(temp.p[temp.length - 2].x+6,
                    temp.p[temp.length - 2].y);
            break;
        case UP:
            temp1= new Point(temp.p[temp.length - 2].x,
                    temp.p[temp.length - 2].y-6);
            break;
        default:
            break;
        }
        if(temp1.y>Game.max)
            temp1.y=Game.min;
        if(temp1.x>Game.max)
            temp1.x=Game.min;
        if(temp1.y<Game.min)
            temp1.y=Game.max;
        if(temp1.x<Game.min)
            temp1.x=Game.max;
        temp.p[temp.length-1]=temp1;
    }

    void readMotion() {
        try {
            while (true) {
                if (Game.changedirection) {
                    String mov = "";
                    mov = Game.move.name();
                    // System.out.println(Game.move);
                    out.writeUTF(mov);
                    out.flush();
                    Game.changedirection = false;
                }
                if (Game.foodeaten) {
                    out.writeUTF("food_eaten");
                    out.flush();
                    Game.foodeaten = false;
                }
                Thread.sleep(50);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    void otherRunMethod() {
        try {
            while (true) {
                String mname = in.readUTF();
                String mov = in.readUTF();
                if (mov.equals("Resigned")) {
                    map.remove(mname);
                } else if (mov.length() >= 10) {
                    if (mov.substring(0, 10).equals("food_eaten")) {
                        String[] s = mov.split(",");
                        Game.foodx = Integer.parseInt(s[1]);
                        Game.foody = Integer.parseInt(s[2]);
                        int score = ++map.get(mname).score;
                        increaseSnakeLength(mname);
                        System.out.println(mname + ":" + score+" Length:"+map.get(mname).length);
                    }
                } else {
                    Game.move = Direction.valueOf(mov);
                    map.get(mname).move = Game.move;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            if (!start) {
                populateMap();
            } else if (start) {
                new Thread(new Runnable() {
                    public void run() {
                        otherRunMethod();
                    }
                }).start();
                readMotion();
                break;
            }
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

代码很长,所以我只是放置了管理连接的代码的服务器端。

package mycode;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.util.Map;

public class Playerhandler implements Runnable {
    Socket player;
    String thisname;
    Map<String, Socket> map;
    DataInputStream in = null;
    DataOutputStream out = null;
    ObjectInputStream ob;
    Snake snake;

    Playerhandler(Socket player, Map<String, Socket> m) {
        this.player = player;
        this.map = m;
        try {
            in = new DataInputStream(new BufferedInputStream(
                    player.getInputStream()));
            thisname = in.readUTF();
            map.put(thisname, this.player);
            populatePlayers();
            System.out.println("Connected Client " + thisname);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    void populatePlayers() {
        try {
            out = new DataOutputStream(new BufferedOutputStream(
                    player.getOutputStream()));
            for (String name : map.keySet()) {
                out.writeUTF(name);
                out.flush();
            }

            for (String name : map.keySet()) {
                out = new DataOutputStream(new BufferedOutputStream(map.get(
                        name).getOutputStream()));
                out.writeUTF(thisname);
                out.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    void relay(String move) {
        try {
            if (move.equals("food_eaten")) {
                move = move + ","
                        + (Snakeserver.randomGenerator.nextInt(100) * 6) + ","
                        + (Snakeserver.randomGenerator.nextInt(100) * 6);

            }
            for (String name : map.keySet()) {
                out = new DataOutputStream(new BufferedOutputStream(map.get(
                        name).getOutputStream()));
                out.writeUTF(thisname);
                out.flush();
                out.writeUTF(move);
                // System.out.println(Direction.valueOf(move));
                out.flush();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void run() {
        while (true) {
            try {
                relay(in.readUTF());
            } catch (IOException e) {
                // TODO Auto-generated catch block
                System.out.println("Player " + thisname + " Resigned");
                map.remove(thisname);
                relay("Resigned");
                return;
            }
        }
    }

}
4

3 回答 3

2

这个答案是回顾对话以达成解决方案,并指出一些其他领域需要研究或尝试。

主要的软件行为问题是拥有多个客户端会导致多个客户端在多次移动后显示不同的蛇位置。

在通过评论的一些问题和回复之后,问题的发布者修改了他们的软件,以便所有客户端都由服务器同步发送所有蛇的对象到所有客户端,以便所有客户端现在使用相同的蛇对象。以前,每个客户端都维护自己的蛇对象数据,并且只接收蛇数据中的更改或增量。通过此更改,所有客户端现在都通过服务器传输的蛇对象同步,但是仍然存在客户端显示稍微不同的位置的问题,该问题在一两分钟后得到纠正,因为每个客户端都会收到所有蛇,客户端再次同步。

下一步是研究一种不同的方法,以便客户端使用 UDP/IP 作为网络传输协议而不是当前使用的 TCP/IP 保持更紧密的同步。使用 UDP/IP 的预期结果是减少TCP 网络传输协议引入的各种滞后,以便提供 TCP 提供的面向连接的、有序的字节流。然而,使用UDP 网络传输协议要求 TCP 使用的一些传递机制以提供可靠的字节序列,必须由 UDP 的用户承担。

UDP 的一些问题是:(1) 数据包的接收顺序可能与发送它们的顺序不同,(2) 数据包可能被丢弃或丢失,因此某些发送的数据包可能无法接收,以及 (3) 数据使用 UDP 发送的必须明确放入数据包中进行传输,以便发送方和接收方看到数据包而不是字节流。

这个贪吃蛇游戏的基本架构如下所示。

客户端将向服务器发送蛇更新。这种交互需要服务器将确认发送回客户端。如果客户端没有收到这样的确认,客户端将在一段时间后重新发送蛇更新。

然后,服务器将更新其数据以反映更改并使用其客户端列表,将相同的数据包发送到所有客户端。每个接收到数据包的客户端都会发送一个确认。通过发送确认,每个客户端通知服务器他们仍在游戏中。如果服务器不再接收客户端确认,它将知道客户端可能已经离开游戏或存在某种网络问题。

每个数据包都会有一个序列号,该序列号在发送数据包后递增。这个序列号提供了一个唯一的标识符,以便客户端和服务器可以检测数据包是否丢失,或者接收到的数据包是否与已经接收到的数据包重复。

对于 UDP,最好是数据包尽可能小。大于底层 IP 网络协议可以发送的 UDP 数据包将被拆分为多个 IP 数据包,多个 IP 数据包一次发送一个,然后在接收网络节点重新组合成 UDP 数据包。

这里有一些关于使用 Java 编程语言的 UDP 网络协议的资源。

课程:关于数据报

一个简单的 Java UDP 服务器和 UDP 客户端

Stackoverflow:在 java 中的 UDP 上发送和接收序列化对象

Java-Gaming.org UDP 与 TCP

Gaffer On Games:每个程序员都需要了解的有关游戏网络的知识

游戏中的灯光师:可靠性和流量控制

Stackoverflow:使用 javaNIO 发送游戏/模拟状态的可能方法有哪些?

于 2013-04-13T00:37:18.533 回答
0

我以前从未实现过网络多人游戏,但我认为这里使用最广泛的“解决方案”是作弊。

我认为它被称为“航位推算”,尽管蛇的工作方式与此完全一样。

http://www.gamasutra.com/view/feature/3230/dead_reckoning_latency_hiding_for_.php

基本上,您将游戏循环与网络更新分离。让每个客户端保持自己的状态,并简单地预测对手在每一帧的位置。然后,当来自服务器的更新到达时,您可以将对手调整到他们的真实位置。为了隐藏这种差异,我认为渲染几毫秒前的游戏状态而不是当前状态是很常见的。这样,网络更新就有更现实的机会赶上游戏循环,因此看起来不那么不稳定。

正如我所说,我自己从来没有真正实现过这个,所以 YMMV。这是游戏开发中的难题之一。

于 2013-03-31T12:33:56.747 回答
0

我倾向于添加对 setTcpNoDelay(true) 的显式调用。这将确保http://en.wikipedia.org/wiki/Nagle%27s_algorithm已关闭,因此禁用优化以提高效率,通常会增加少量延迟。

于 2013-03-31T12:52:58.930 回答