一个里程碑(可以联机了)

This commit is contained in:
2025-09-24 20:07:32 +08:00
parent 5f080713f8
commit 659827bcc3
20 changed files with 282 additions and 288 deletions

View File

@@ -9,7 +9,7 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas;
public class Alice extends Fighter { public class Alice extends Fighter {
public Alice() { public Alice() {
super(new TextureAtlas(Gdx.files.internal("src/main/resources/character/alice/精灵1.2.atlas"))); super("Alice", new TextureAtlas(Gdx.files.internal("src/main/resources/character/alice/精灵1.2.atlas")));
// 设置角色属性 // 设置角色属性
speed = 350f; // 更快的移动速度 speed = 350f; // 更快的移动速度
@@ -20,22 +20,27 @@ public class Alice extends Fighter {
@Override @Override
protected void loadAnimations() { protected void loadAnimations() {
// 加载基础动作动画 // 基础动作
loadAnimationFromAtlas(Action.IDLE, "stand/stand", 15, true); loadLoopingAnimation(Action.IDLE, "stand/stand", 15);
loadAnimationFromAtlas(Action.WALK, "walkFront/walkFront", 9, true); loadLoopingAnimation(Action.WALK, "walkFront/walkFront", 9);
loadAnimationFromAtlas(Action.JUMP, "jump/jump", 8, false); loadOneShotAnimation(Action.JUMP, "jump/jump", 8);
loadAnimationFromAtlas(Action.FALL, "hitSpin/hitSpin", 5, false); loadOneShotAnimation(Action.FALL, "hitSpin/hitSpin", 5);
// 加载攻击动作动画 // 攻击动作
loadAnimationFromAtlas(Action.ATTACK1, "attackAa/attackAa", 6, false); loadOneShotAnimation(Action.ATTACK1, "attackAa/attackAa", 6);
loadAnimationFromAtlas(Action.ATTACK2, "attackAb/attackAb", 6, false); loadOneShotAnimation(Action.ATTACK2, "attackAb/attackAb", 6);
loadAnimationFromAtlas(Action.ATTACK3, "attackAc/attackAc", 6, false); loadOneShotAnimation(Action.ATTACK3, "attackAc/attackAc", 6);
loadAnimationFromAtlas(Action.ATTACK4, "attackAd/attackAd", 6, false); loadOneShotAnimation(Action.ATTACK4, "attackAd/attackAd", 6);
// 加载受击动画 // // 特殊动作(可扩展)
loadAnimationFromAtlas(Action.HIT, "hitSpin/hitSpin", 5, false); // loadOneShotAnimation(Action.SPECIAL1, "special/special1", 6);
// loadOneShotAnimation(Action.SPECIAL2, "special/special2", 6);
// 设置帧间隔(动作速度) // 受击与死亡
loadOneShotAnimation(Action.HIT, "hitSpin/hitSpin", 5);
// loadOneShotAnimation(Action.DEATH, "death/death", 8);
// 帧速率调整
setFrameDuration(Action.IDLE, 0.04f); setFrameDuration(Action.IDLE, 0.04f);
setFrameDuration(Action.WALK, 0.08f); setFrameDuration(Action.WALK, 0.08f);
setFrameDuration(Action.ATTACK1, 0.07f); setFrameDuration(Action.ATTACK1, 0.07f);
@@ -50,6 +55,7 @@ public class Alice extends Fighter {
if (currentAction != Action.ATTACK1 && if (currentAction != Action.ATTACK1 &&
currentAction != Action.ATTACK2 && currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 && currentAction != Action.ATTACK3 &&
currentAction != Action.ATTACK4 &&
currentAction != Action.SPECIAL1 && currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 && currentAction != Action.SPECIAL2 &&
currentAction != Action.DEFEND && currentAction != Action.DEFEND &&

View File

@@ -1,21 +1,14 @@
package uno.mloluyu.characters; package uno.mloluyu.characters;
import uno.mloluyu.util.SimpleFormatter;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.Disposable;
import uno.mloluyu.util.SimpleFormatter;
import java.util.EnumMap; import java.util.EnumMap;
/**
* 格斗角色父类,封装所有角色共有的动画和状态管理逻辑
*/
public abstract class Fighter implements Disposable { public abstract class Fighter implements Disposable {
public enum Action { public enum Action {
@@ -51,10 +44,10 @@ public abstract class Fighter implements Disposable {
protected float scaleX = 1.0f; protected float scaleX = 1.0f;
protected float scaleY = 1.0f; protected float scaleY = 1.0f;
public Fighter() { public Fighter() {}
}
public Fighter(TextureAtlas atlas) { public Fighter(String name, TextureAtlas atlas) {
this.name = name;
this.atlas = atlas; this.atlas = atlas;
for (Action action : Action.values()) { for (Action action : Action.values()) {
frameDurations.put(action, DEFAULT_FRAME_DURATION); frameDurations.put(action, DEFAULT_FRAME_DURATION);
@@ -85,6 +78,14 @@ public abstract class Fighter implements Disposable {
animations.put(action, animation); animations.put(action, animation);
} }
protected void loadLoopingAnimation(Action action, String prefix, int count) {
loadAnimationFromAtlas(action, prefix, count, true);
}
protected void loadOneShotAnimation(Action action, String prefix, int count) {
loadAnimationFromAtlas(action, prefix, count, false);
}
protected void setFrameDuration(Action action, float duration) { protected void setFrameDuration(Action action, float duration) {
frameDurations.put(action, duration); frameDurations.put(action, duration);
Animation<TextureRegion> anim = animations.get(action); Animation<TextureRegion> anim = animations.get(action);
@@ -103,23 +104,12 @@ public abstract class Fighter implements Disposable {
} }
protected void handleAnimationTransitions() { protected void handleAnimationTransitions() {
if (!isAnimationFinished) if (!isAnimationFinished) return;
return;
switch (currentAction) { switch (currentAction) {
case ATTACK1: case ATTACK1, ATTACK2, ATTACK3, SPECIAL1, SPECIAL2, HIT -> changeAction(Action.IDLE);
case ATTACK2: case JUMP -> changeAction(Action.FALL);
case ATTACK3: default -> {}
case SPECIAL1:
case SPECIAL2:
case HIT:
changeAction(Action.IDLE);
break;
case JUMP:
changeAction(Action.FALL);
break;
default:
break;
} }
} }
@@ -150,8 +140,7 @@ public abstract class Fighter implements Disposable {
} }
public boolean changeAction(Action newAction) { public boolean changeAction(Action newAction) {
if (isActionUninterruptible(currentAction)) if (isActionUninterruptible(currentAction)) return false;
return false;
if (currentAction != newAction) { if (currentAction != newAction) {
currentAction = newAction; currentAction = newAction;
stateTime = 0f; stateTime = 0f;
@@ -182,18 +171,17 @@ public abstract class Fighter implements Disposable {
protected void handleMoveState() { protected void handleMoveState() {
if (!isActionUninterruptible(currentAction) && if (!isActionUninterruptible(currentAction) &&
currentAction != Action.JUMP && currentAction != Action.JUMP &&
currentAction != Action.FALL && currentAction != Action.FALL &&
currentAction != Action.DEFEND && currentAction != Action.DEFEND &&
!currentAction.name().startsWith("ATTACK") && !currentAction.name().startsWith("ATTACK") &&
!currentAction.name().startsWith("SPECIAL")) { !currentAction.name().startsWith("SPECIAL")) {
changeAction(Action.WALK); changeAction(Action.WALK);
} }
} }
public boolean attack(int attackType) { public boolean attack(int attackType) {
if (!canAttack()) if (!canAttack()) return false;
return false;
Action attackAction = switch (attackType) { Action attackAction = switch (attackType) {
case 1 -> Action.ATTACK1; case 1 -> Action.ATTACK1;
@@ -212,13 +200,24 @@ public abstract class Fighter implements Disposable {
} }
public void takeHit(int damage) { public void takeHit(int damage) {
if (currentAction == Action.DEATH) if (currentAction == Action.DEATH) return;
return;
health = Math.max(0, health - damage); health = Math.max(0, health - damage);
changeAction(health == 0 ? Action.DEATH : Action.HIT); changeAction(health == 0 ? Action.DEATH : Action.HIT);
} }
public void setPosition(float x, float y) {
hitbox.setPosition(x, y);
}
public void setFacingRight(boolean facingRight) {
this.isFacingRight = facingRight;
}
public String getName() {
return name;
}
public Rectangle getHitbox() { public Rectangle getHitbox() {
return hitbox; return hitbox;
} }
@@ -235,12 +234,6 @@ public abstract class Fighter implements Disposable {
return health; return health;
} }
@Override
public void dispose() {
if (atlas != null)
atlas.dispose();
}
public Action getCurrentAction() { public Action getCurrentAction() {
return currentAction; return currentAction;
} }
@@ -260,4 +253,9 @@ public abstract class Fighter implements Disposable {
public float getCenterY() { public float getCenterY() {
return hitbox.y + hitbox.height / 2; return hitbox.y + hitbox.height / 2;
} }
@Override
public void dispose() {
if (atlas != null) atlas.dispose();
}
} }

View File

@@ -1,5 +1,8 @@
package uno.mloluyu.desktop; package uno.mloluyu.desktop;
import java.util.UUID;
import uno.mloluyu.network.NetworkManager;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter; import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Color;
@@ -141,6 +144,7 @@ public class CharacterSelectScreen extends ScreenAdapter {
} }
} }
// 点击确认按钮
// 点击确认按钮 // 点击确认按钮
if (isHovered(mouseX, mouseY, BUTTON_X, CONFIRM_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) { if (isHovered(mouseX, mouseY, BUTTON_X, CONFIRM_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) {
if (selectedIndex != -1) { if (selectedIndex != -1) {
@@ -155,12 +159,19 @@ public class CharacterSelectScreen extends ScreenAdapter {
case "Reimu": case "Reimu":
fighter = new Reimu(); fighter = new Reimu();
break; break;
// case "弓箭手":
// fighter = new Archer();
// break;
} }
if (fighter != null) { if (fighter != null) {
if (multiplayerMode) {
// 设置唯一玩家 ID 并发送角色选择
if (NetworkManager.getInstance().getLocalPlayerId() == null) {
String playerId = UUID.randomUUID().toString();
NetworkManager.getInstance().setLocalPlayerId(playerId);
Gdx.app.log("Network", "设置玩家ID: " + playerId);
}
NetworkManager.getInstance().sendCharacterSelection(selectedCharacter);
}
game.setScreen(new GameScreen(game, fighter)); game.setScreen(new GameScreen(game, fighter));
} }
} }

View File

@@ -1,30 +1,41 @@
package uno.mloluyu.desktop; package uno.mloluyu.desktop;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter; import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.Fighter; import uno.mloluyu.characters.Fighter;
import uno.mloluyu.characters.Reimu;
import uno.mloluyu.network.NetworkManager; import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen; import uno.mloluyu.util.ClearScreen;
import uno.mloluyu.versatile.FighterController;
public class GameScreen extends ScreenAdapter { public class GameScreen extends ScreenAdapter {
private final MainGame game; private final MainGame game;
private final Fighter player; private final Fighter player;
private SpriteBatch batch; private SpriteBatch batch;
private ShapeRenderer shapeRenderer; private ShapeRenderer shapeRenderer;
private FighterController controller;
private final Map<String, Fighter> otherPlayers = new HashMap<>();
public GameScreen(MainGame game, Fighter player) { public GameScreen(MainGame game, Fighter player) {
this.game = game; this.game = game;
this.player = player; this.player = player;
this.controller = new FighterController(player);
} }
@Override @Override
public void show() { public void show() {
batch = new SpriteBatch(); batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer(); shapeRenderer = new ShapeRenderer();
Gdx.input.setInputProcessor(controller);
} }
@Override @Override
@@ -33,7 +44,7 @@ public class GameScreen extends ScreenAdapter {
// 更新角色状态 // 更新角色状态
player.update(delta); player.update(delta);
controller.update(delta);
// 发送本机玩家位置 // 发送本机玩家位置
if (NetworkManager.getInstance().isConnected()) { if (NetworkManager.getInstance().isConnected()) {
NetworkManager.getInstance().sendPosition(player.getX(), player.getY()); NetworkManager.getInstance().sendPosition(player.getX(), player.getY());
@@ -42,40 +53,56 @@ public class GameScreen extends ScreenAdapter {
// 渲染角色 // 渲染角色
batch.begin(); batch.begin();
player.render(batch); player.render(batch);
batch.end();
// 渲染其他玩家位置(联机模式) // 渲染其他玩家位置(联机模式)
if (NetworkManager.getInstance().isConnected()) { if (NetworkManager.getInstance().isConnected()) {
renderOtherPlayers(); renderOtherPlayers();
} }
batch.end();
} }
private void renderOtherPlayers() { private void renderOtherPlayers() {
// 获取其他玩家位置并渲染 Map<String, float[]> positions = NetworkManager.getInstance().getPlayerPositions();
for (String position : NetworkManager.getInstance().getOtherPlayerPositions()) { Map<String, String> characters = NetworkManager.getInstance().getPlayerCharacters();
String[] coords = position.split(",");
if (coords.length == 2) {
try {
float x = Float.parseFloat(coords[0]);
float y = Float.parseFloat(coords[1]);
// 使用形状渲染器绘制其他玩家标记 batch.begin();
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); for (Map.Entry<String, float[]> entry : positions.entrySet()) {
shapeRenderer.setColor(Color.RED); String playerId = entry.getKey();
shapeRenderer.circle(x, y, 20); // 红色圆圈表示其他玩家 float[] pos = entry.getValue();
shapeRenderer.end();
// 在批处理中绘制文字 // 跳过本机玩家(可选)
batch.begin(); // if (playerId.equals(NetworkManager.getInstance().getLocalPlayerId()))
// 这里可以添加玩家名字显示 // continue;
batch.end();
} catch (NumberFormatException e) { // 获取角色名
Gdx.app.error("GameScreen", "解析玩家位置失败: " + position); String characterName = characters.get(playerId);
if (characterName == null || pos == null)
continue;
// 获取或创建 Fighter 实例
Fighter fighter = otherPlayers.get(playerId);
if (fighter == null) {
switch (characterName) {
case "Alice":
fighter = new Alice();
break;
case "Reimu":
fighter = new Reimu();
default:
fighter = new Alice();
break;
};
if (fighter != null) {
otherPlayers.put(playerId, fighter);
} }
} }
// 设置位置并渲染
if (fighter != null) {
fighter.setPosition(pos[0], pos[1]);
fighter.render(batch);
}
} }
batch.end();
} }
@Override @Override

View File

@@ -8,8 +8,7 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import uno.mloluyu.network.ConnectClient; import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.network.ConnectServer;
import static uno.mloluyu.util.Font.loadChineseFont; import static uno.mloluyu.util.Font.loadChineseFont;
import uno.mloluyu.util.ClearScreen; import uno.mloluyu.util.ClearScreen;
@@ -24,7 +23,7 @@ public class NetworkSettingsScreen extends ScreenAdapter {
private static final int BUTTON_X = 760; private static final int BUTTON_X = 760;
private static final int CREATE_ROOM_Y = 500; private static final int CREATE_ROOM_Y = 500;
private static final int JOIN_ROOM_Y = 380; private static final int JOIN_ROOM_Y = 380;
private static final int EXIT_Y = 260; // 退出按钮位置 private static final int EXIT_Y = 260;
public NetworkSettingsScreen(MainGame game) { public NetworkSettingsScreen(MainGame game) {
this.game = game; this.game = game;
@@ -48,7 +47,6 @@ public class NetworkSettingsScreen extends ScreenAdapter {
renderButtons(mouseX, mouseY); renderButtons(mouseX, mouseY);
renderTexts(); renderTexts();
handleInput(mouseX, mouseY); handleInput(mouseX, mouseY);
} }
@@ -56,7 +54,7 @@ public class NetworkSettingsScreen extends ScreenAdapter {
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
drawButton(CREATE_ROOM_Y, mouseX, mouseY); drawButton(CREATE_ROOM_Y, mouseX, mouseY);
drawButton(JOIN_ROOM_Y, mouseX, mouseY); drawButton(JOIN_ROOM_Y, mouseX, mouseY);
drawButton(EXIT_Y, mouseX, mouseY); // 新增退出按钮 drawButton(EXIT_Y, mouseX, mouseY);
shapeRenderer.end(); shapeRenderer.end();
} }
@@ -65,37 +63,36 @@ public class NetworkSettingsScreen extends ScreenAdapter {
font.draw(batch, "联机设置", BUTTON_X + 100, 650); font.draw(batch, "联机设置", BUTTON_X + 100, 650);
drawButtonText(CREATE_ROOM_Y, "创建房间"); drawButtonText(CREATE_ROOM_Y, "创建房间");
drawButtonText(JOIN_ROOM_Y, "加入房间"); drawButtonText(JOIN_ROOM_Y, "加入房间");
drawButtonText(EXIT_Y, "返回"); // 新增退出按钮文字 drawButtonText(EXIT_Y, "返回");
batch.end(); batch.end();
} }
private void handleInput(int mouseX, int mouseY) { private void handleInput(int mouseX, int mouseY) {
if (Gdx.input.justTouched()) { if (Gdx.input.justTouched()) {
// 创建房间
if (isHovered(mouseX, mouseY, BUTTON_X, CREATE_ROOM_Y)) { if (isHovered(mouseX, mouseY, BUTTON_X, CREATE_ROOM_Y)) {
Gdx.app.log("Network", "创建房间按钮被点击!"); Gdx.app.log("Network", "创建房间按钮被点击!");
new Thread(new ConnectServer(11455)).start();
// 添加服务器启动提示信息 NetworkManager.getInstance().createRoom();
Gdx.app.log("Network", "服务器已启动,等待玩家连接..."); NetworkManager.getInstance().joinRoom("127.0.0.1");
System.out.println("服务器已启动,等待玩家连接...");
// 创建联机模式标识并传递到角色选择界面 Gdx.app.log("Network", "已连接到本地服务器,等待其他玩家加入...");
CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game); CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game);
characterSelectScreen.setMultiplayerMode(true); // 设置为联机模式 characterSelectScreen.setMultiplayerMode(true);
game.setScreen(characterSelectScreen); game.setScreen(characterSelectScreen);
}
} else if (isHovered(mouseX, mouseY, BUTTON_X, JOIN_ROOM_Y)) { // 加入房间
else if (isHovered(mouseX, mouseY, BUTTON_X, JOIN_ROOM_Y)) {
Gdx.app.log("Network", "加入房间按钮被点击!"); Gdx.app.log("Network", "加入房间按钮被点击!");
// 使用LibGDX的输入对话框避免AWT线程问题
Gdx.input.getTextInput(new com.badlogic.gdx.Input.TextInputListener() { Gdx.input.getTextInput(new com.badlogic.gdx.Input.TextInputListener() {
@Override @Override
public void input(String ip) { public void input(String ip) {
if (ip != null && !ip.trim().isEmpty()) { if (ip != null && !ip.trim().isEmpty()) {
new Thread(() -> new ConnectClient(ip.trim(), 11455)).start(); NetworkManager.getInstance().joinRoom(ip.trim());
Gdx.app.log("Network", "正在连接到服务器 " + ip.trim() + "..."); Gdx.app.log("Network", "正在连接到服务器 " + ip.trim() + "...");
System.out.println("正在连接到服务器 " + ip.trim() + "...");
// 使用postRunnable确保在主线程中执行屏幕切换
Gdx.app.postRunnable(() -> { Gdx.app.postRunnable(() -> {
CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game); CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game);
characterSelectScreen.setMultiplayerMode(true); characterSelectScreen.setMultiplayerMode(true);
@@ -109,10 +106,12 @@ public class NetworkSettingsScreen extends ScreenAdapter {
Gdx.app.log("Network", "用户取消输入 IP"); Gdx.app.log("Network", "用户取消输入 IP");
} }
}, "请输入服务器 IP 地址", "", "加入房间"); }, "请输入服务器 IP 地址", "", "加入房间");
}
} else if (isHovered(mouseX, mouseY, BUTTON_X, EXIT_Y)) { // 返回主菜单
else if (isHovered(mouseX, mouseY, BUTTON_X, EXIT_Y)) {
Gdx.app.log("Network", "退出按钮被点击!"); Gdx.app.log("Network", "退出按钮被点击!");
game.setScreen(new MainMenuScreen(game)); // 或者 Gdx.app.exit(); 直接退出游戏 game.setScreen(new MainMenuScreen(game));
} }
} }
} }
@@ -139,5 +138,4 @@ public class NetworkSettingsScreen extends ScreenAdapter {
font.dispose(); font.dispose();
shapeRenderer.dispose(); shapeRenderer.dispose();
} }
} }

View File

@@ -8,7 +8,7 @@ import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* 客户端连接类(实例方式) * 客户端连接类
*/ */
public class ConnectClient { public class ConnectClient {
private Socket socket; private Socket socket;
@@ -20,48 +20,40 @@ public class ConnectClient {
// 启动接收线程 // 启动接收线程
new Thread(this::receiveMessages).start(); new Thread(this::receiveMessages).start();
// 示例:发送一条欢迎消息
sendMessage("你好,我是客户端玩家");
} catch (Exception e) { } catch (Exception e) {
Gdx.app.error("Client", "连接失败: " + e.getMessage(), e); Gdx.app.error("Client", "连接失败: " + e.getMessage(), e);
} }
} }
// ... 现有代码 ... private void receiveMessages() {
try {
byte[] buffer = new byte[1024];
while (true) {
int read = socket.getInputStream().read(buffer);
if (read == -1)
break;
private void receiveMessages() { String message = new String(buffer, 0, read, StandardCharsets.UTF_8);
try { Gdx.app.log("Client", "收到服务器消息: " + message);
byte[] buffer = new byte[1024];
while (true) {
int read = socket.getInputStream().read(buffer);
if (read == -1) break;
String message = new String(buffer, 0, read, StandardCharsets.UTF_8);
Gdx.app.log("Client", "收到服务器消息: " + message);
// 处理位置消息 // 主线程处理消息,避免线程冲突
if (message.startsWith("POS:")) { Gdx.app.postRunnable(() -> {
String positionData = message.substring(4); NetworkManager.getInstance().receiveMessage(message);
// 通知网络管理器更新其他玩家位置 });
NetworkManager.getInstance().updatePlayerPosition(positionData);
} }
} catch (Exception e) {
Gdx.app.error("Client", "接收消息异常: " + e.getMessage(), e);
} finally {
disconnect();
} }
} catch (Exception e) {
Gdx.app.error("Client", "接收消息异常: " + e.getMessage(), e);
} finally {
disconnect();
} }
}
// ... 现有代码 ...
public void sendMessage(String message) { public void sendMessage(String message) {
try { try {
OutputStream out = socket.getOutputStream(); OutputStream out = socket.getOutputStream();
out.write(message.getBytes(StandardCharsets.UTF_8)); out.write(message.getBytes(StandardCharsets.UTF_8));
out.flush(); out.flush();
Gdx.app.log("Client", "发送消息: " + message);
} catch (Exception e) { } catch (Exception e) {
Gdx.app.error("Client", "发送消息失败: " + e.getMessage(), e); Gdx.app.error("Client", "发送消息失败: " + e.getMessage(), e);
} }

View File

@@ -6,41 +6,16 @@ import com.badlogic.gdx.net.ServerSocket;
import com.badlogic.gdx.net.Socket; import com.badlogic.gdx.net.Socket;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/**
* 支持两个玩家连接的服务器类
*/
public class ConnectServer implements Runnable { public class ConnectServer implements Runnable {
private final int port; private final int port;
private ServerSocket serverSocket; private ServerSocket serverSocket;
private final List<Player> connectedPlayers = new ArrayList<>(); private final List<Socket> connectedSockets = new ArrayList<>();
private static final int MAX_PLAYERS = 2; private static final int MAX_PLAYERS = 2;
// 玩家内部类存储Socket和玩家名字
private static class Player {
Socket socket;
String playerName;
Player(Socket socket) {
this.socket = socket;
this.playerName = "玩家" + (int) (Math.random() * 1000); // 默认随机名字
}
String getPlayerName() {
return playerName;
}
void setPlayerName(String name) {
this.playerName = name;
}
Socket getSocket() {
return socket;
}
}
public ConnectServer(int port) { public ConnectServer(int port) {
this.port = port; this.port = port;
} }
@@ -51,106 +26,65 @@ public class ConnectServer implements Runnable {
Gdx.app.log("Server", "服务器已启动,等待玩家连接..."); Gdx.app.log("Server", "服务器已启动,等待玩家连接...");
try { try {
while (connectedPlayers.size() < MAX_PLAYERS) { while (connectedSockets.size() < MAX_PLAYERS) {
Socket socket = serverSocket.accept(null); Socket socket = serverSocket.accept(null);
Player player = new Player(socket); connectedSockets.add(socket);
connectedPlayers.add(player); Gdx.app.log("Server", "玩家连接成功: " + socket.getRemoteAddress());
Gdx.app.log("Server", "玩家连接成功: " + socket.getRemoteAddress() + ",默认名字: " + player.getPlayerName()); new Thread(() -> handlePlayer(socket)).start();
new Thread(() -> handlePlayer(player)).start();
} }
Gdx.app.log("Server", "已连接两个玩家,游戏准备开始!"); Gdx.app.log("Server", "已连接两个玩家,游戏准备开始!");
// 输出当前所有玩家名字
for (int i = 0; i < connectedPlayers.size(); i++) {
Gdx.app.log("Server", "玩家" + (i + 1) + ": " + connectedPlayers.get(i).getPlayerName());
}
} catch (Exception e) { } catch (Exception e) {
Gdx.app.error("Server", "连接异常: " + e.getMessage(), e); Gdx.app.error("Server", "连接异常: " + e.getMessage(), e);
} }
} }
// ... 现有代码 ... private void handlePlayer(Socket socket) {
private void handlePlayer(Player player) {
try { try {
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
while (true) { while (true) {
int read = player.getSocket().getInputStream().read(buffer); int read = socket.getInputStream().read(buffer);
if (read == -1) if (read == -1)
break; break;
String message = new String(buffer, 0, read);
Gdx.app.log("Server", "收到玩家" + player.getPlayerName() + "的消息: " + message);
// 处理设置玩家名字的消息 String message = new String(buffer, 0, read, StandardCharsets.UTF_8);
if (message.startsWith("SET_NAME:")) { Gdx.app.log("Server", "收到消息: " + message);
String newName = message.substring(8);
player.setPlayerName(newName); broadcastToOthers(socket, message);
Gdx.app.log("Server", "玩家名字已更新为: " + newName);
} Gdx.app.postRunnable(() -> {
// 处理位置消息并广播 NetworkManager.getInstance().receiveMessage(message);
else if (message.startsWith("POS:")) { });
String positionData = message.substring(4);
// 广播给其他玩家
broadcastToOtherPlayers(player, "POS:" + positionData);
}
} }
} catch (Exception e) { } catch (Exception e) {
Gdx.app.error("Server", "玩家通信异常: " + e.getMessage(), e); Gdx.app.error("Server", "玩家通信异常: " + e.getMessage(), e);
} finally { } finally {
player.getSocket().dispose(); socket.dispose();
connectedPlayers.remove(player); connectedSockets.remove(socket);
Gdx.app.log("Server", "玩家" + player.getPlayerName() + "断开连接"); Gdx.app.log("Server", "玩家断开连接");
} }
} }
// 新增广播方法 public void broadcastToOthers(Socket sender, String message) {
private void broadcastToOtherPlayers(Player sender, String message) { for (Socket socket : connectedSockets) {
for (Player player : connectedPlayers) { if (socket != sender) {
if (player != sender) {
try { try {
OutputStream out = player.getSocket().getOutputStream(); OutputStream out = socket.getOutputStream();
out.write(message.getBytes(java.nio.charset.StandardCharsets.UTF_8)); out.write(message.getBytes(StandardCharsets.UTF_8));
out.flush(); out.flush();
Gdx.app.log("Server", "广播消息到 " + socket.getRemoteAddress() + ": " + message);
} catch (Exception e) { } catch (Exception e) {
Gdx.app.error("Server", "广播消息失败: " + e.getMessage(), e); Gdx.app.error("Server", "广播失败: " + e.getMessage(), e);
} }
} }
} }
} }
// ... 现有代码 ...
// 获取当前服务器所有玩家名字的方法
public List<String> getPlayerNames() {
List<String> names = new ArrayList<>();
for (Player player : connectedPlayers) {
names.add(player.getPlayerName());
}
return names;
}
// 根据索引获取特定玩家名字的方法
public String getPlayerName(int index) {
if (index >= 0 && index < connectedPlayers.size()) {
return connectedPlayers.get(index).getPlayerName();
}
return null;
}
// 设置玩家名字的方法
public void setPlayerName(int index, String name) {
if (index >= 0 && index < connectedPlayers.size()) {
connectedPlayers.get(index).setPlayerName(name);
Gdx.app.log("Server", "玩家" + index + "名字设置为: " + name);
}
}
public void dispose() { public void dispose() {
for (Player player : connectedPlayers) { for (Socket socket : connectedSockets) {
player.getSocket().dispose(); socket.dispose();
} }
connectedPlayers.clear(); connectedSockets.clear();
if (serverSocket != null) { if (serverSocket != null) {
serverSocket.dispose(); serverSocket.dispose();

View File

@@ -1,18 +1,20 @@
package uno.mloluyu.network; package uno.mloluyu.network;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import java.util.ArrayList;
import java.util.List;
/** import java.util.HashMap;
* 网络管理器,协调服务器和客户端通信 import java.util.Map;
*/
public class NetworkManager { public class NetworkManager {
private static NetworkManager instance; private static NetworkManager instance;
private ConnectServer server; private ConnectServer server;
private ConnectClient client; private ConnectClient client;
private boolean isHost = false; private boolean isHost = false;
private List<String> playerPositions = new ArrayList<>();
private String localPlayerId;
private String localCharacter;
private final Map<String, float[]> playerPositions = new HashMap<>();
private final Map<String, String> playerCharacters = new HashMap<>();
public static NetworkManager getInstance() { public static NetworkManager getInstance() {
if (instance == null) { if (instance == null) {
@@ -21,67 +23,91 @@ public class NetworkManager {
return instance; return instance;
} }
/** public void setLocalPlayerId(String id) {
* 创建房间(作为主机) this.localPlayerId = id;
*/ }
public String getLocalPlayerId() {
return localPlayerId;
}
public void createRoom() { public void createRoom() {
isHost = true; isHost = true;
server = new ConnectServer(11455); server = new ConnectServer(11455);
new Thread(server).start(); new Thread(server).start();
Gdx.app.log("Network", "间创建成功,等待其他玩家加入..."); Gdx.app.log("Network", "主模式:服务器已启动");
} }
/**
* 加入房间(作为客户端)
*/
public void joinRoom(String ip) { public void joinRoom(String ip) {
isHost = false; isHost = false;
client = new ConnectClient(ip, 11455); client = new ConnectClient(ip, 11455);
Gdx.app.log("Network", "正在加入房间: " + ip); Gdx.app.log("Network", "客户端模式:连接到房主 " + ip);
} }
/**
* 发送玩家位置信息
*/
public void sendPosition(float x, float y) { public void sendPosition(float x, float y) {
String msg = "POS:" + localPlayerId + "," + x + "," + y;
Gdx.app.log("Network", "发送位置消息: " + msg);
if (isHost && server != null) { if (isHost && server != null) {
// 主机直接广播位置 server.broadcastToOthers(null, msg);
broadcastMessage("POS:" + x + "," + y); receiveMessage(msg); // 房主自己也处理
} else if (client != null) { } else if (client != null) {
// 客户端发送位置到服务器 client.sendMessage(msg);
client.sendMessage("POS:" + x + "," + y);
} }
} }
/** public void sendCharacterSelection(String character) {
* 广播消息(主机使用) this.localCharacter = character;
*/ String msg = "SELECT:" + localPlayerId + "," + character;
private void broadcastMessage(String message) { Gdx.app.log("Network", "发送角色选择消息: " + msg);
// 这里需要实现广播逻辑 if (isHost && server != null) {
Gdx.app.log("Network", "广播消息: " + message); server.broadcastToOthers(null, msg);
receiveMessage(msg);
} else if (client != null) {
client.sendMessage(msg);
}
} }
/** public void receiveMessage(String message) {
* 获取其他玩家位置 Gdx.app.log("Network", "收到消息: " + message);
*/
public List<String> getOtherPlayerPositions() { if (message.startsWith("POS:")) {
String[] parts = message.substring(4).split(",");
if (parts.length == 3) {
String playerId = parts[0];
try {
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
playerPositions.put(playerId, new float[]{x, y});
Gdx.app.log("Network", "位置更新: " + playerId + " -> " + x + "," + y);
} catch (NumberFormatException e) {
Gdx.app.error("Network", "位置解析失败: " + message);
}
} else {
Gdx.app.error("Network", "位置消息格式错误: " + message);
}
} else if (message.startsWith("SELECT:")) {
String[] parts = message.substring(7).split(",");
if (parts.length == 2) {
String playerId = parts[0];
String character = parts[1];
playerCharacters.put(playerId, character);
Gdx.app.log("Network", "角色选择: " + playerId + " -> " + character);
} else {
Gdx.app.error("Network", "角色选择消息格式错误: " + message);
}
} else if (message.equals("READY")) {
Gdx.app.log("Network", "收到准备信号");
} else {
Gdx.app.log("Network", "未知消息类型: " + message);
}
}
public Map<String, float[]> getPlayerPositions() {
return playerPositions; return playerPositions;
} }
/** public Map<String, String> getPlayerCharacters() {
* 断开连接 return playerCharacters;
*/
public void disconnect() {
if (server != null) {
server.dispose();
server = null;
}
if (client != null) {
client.disconnect();
client = null;
}
playerPositions.clear();
Gdx.app.log("Network", "网络连接已断开");
} }
public boolean isHost() { public boolean isHost() {
@@ -92,15 +118,17 @@ public class NetworkManager {
return server != null || client != null; return server != null || client != null;
} }
public void updatePlayerPosition(String positionData) { public void disconnect() {
// 简单实现:直接存储位置数据 if (server != null) {
playerPositions.add(positionData); server.dispose();
server = null;
// 限制位置列表大小,避免内存泄漏
if (playerPositions.size() > 10) {
playerPositions.remove(0);
} }
if (client != null) {
Gdx.app.log("Network", "更新其他玩家位置: " + positionData); client.disconnect();
client = null;
}
playerPositions.clear();
playerCharacters.clear();
Gdx.app.log("Network", "已断开连接");
} }
} }