添加 UI 资源并重构角色类

添加了新的 UI 资源:logo.png、uiskin.atlas 和 uiskin.json,以改进界面设计。
移除了过时的 FighterController 和 GameCore 类,以精简代码库。
引入了新的角色类:FighterList 和 Reimu,增加了角色选择选项。
实现了新的桌面屏幕:CharacterSelectScreen(角色选择屏幕)、GameScreen(游戏屏幕)、MainMenuScreen(主菜单屏幕)和 StartScreen(开始屏幕),以改善用户导航。
通过新的 ConnectClient、ConnectServer 和 NetworkManager 类建立了网络功能。
更新了工具类:ClearScreen、Font 和 SimpleFormatter,以提升功能。
创建了新的 ButtonActions 类来处理按钮交互。
This commit is contained in:
2025-09-23 21:46:12 +08:00
parent 7a47759cf4
commit 5f080713f8
85 changed files with 7379 additions and 1651 deletions

View File

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"kingleo.qwen"
]
}

31
pom.xml
View File

@@ -20,6 +20,37 @@
</properties>
<dependencies>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version> <!-- 替换为你的 LibGDX 版本 -->
</dependency>
<!-- FreeType 字体扩展 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.12.1</version> <!-- 与 LibGDX 版本保持一致 -->
</dependency>
<!-- 桌面平台的 FreeType 原生库 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>1.12.1</version> <!-- 使用你的 LibGDX 版本号 -->
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>

View File

@@ -4,57 +4,73 @@ import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
/**
* 角色类继承自Fighter父类
* Alice角色类继承自Fighter父类,定义其专属属性和动画
*/
public class Alice extends Fighter {
private TextureAtlas atlas;
public Alice() {
super(new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\alice\\精灵1.2.atlas")));
speed = 350f; // 速度更快
maxHealth = 90; // 生命值较低
health = maxHealth;
attackPower = 12; // 攻击力中等\
super(new TextureAtlas(Gdx.files.internal("src/main/resources/character/alice/精灵1.2.atlas")));
// 设置角色属性
speed = 350f; // 更快的移动速度
maxHealth = 90; // 较低的生命值
health = maxHealth;
attackPower = 12; // 中等攻击力
}
@Override
protected void loadAnimations() {
// 加载基础动作动画
loadAnimationFromAtlas(Action.IDLE, "stand/stand", 15, true);
loadAnimationFromAtlas(Action.WALK, "walkFront/walkFront", 9, true);
loadAnimationFromAtlas(Action.JUMP, "jump/jump", 8, false);
loadAnimationFromAtlas(Action.FALL, "hitSpin/hitSpin", 5, false);
// 加载攻击动作动画
loadAnimationFromAtlas(Action.ATTACK1, "attackAa/attackAa", 6, false);
loadAnimationFromAtlas(Action.ATTACK2, "attackAb/attackAb", 6, false);
loadAnimationFromAtlas(Action.ATTACK3, "attackAc/attackAc", 6, false);
loadAnimationFromAtlas(Action.ATTACK4, "attackAd/attackAd", 6, false);
// 加载受击动画
loadAnimationFromAtlas(Action.HIT, "hitSpin/hitSpin", 5, false);
// 为特定动作设置帧间隔
// 设置帧间隔(动作速度)
setFrameDuration(Action.IDLE, 0.04f);
setFrameDuration(Action.WALK, 0.08f); // 行走更快
setFrameDuration(Action.ATTACK1, 0.07f); // 攻击更快
setFrameDuration(Action.SPECIAL2, 0.06f); // 特殊技能2非常快
setFrameDuration(Action.WALK, 0.08f);
setFrameDuration(Action.ATTACK1, 0.07f);
setFrameDuration(Action.SPECIAL2, 0.06f);
}
// 特定的移动状态处理
/**
* 特定移动状态处理:允许跳跃状态下移动
*/
@Override
protected void handleMoveState() {
// 在跳跃时也能移动
if (currentAction != Action.ATTACK1 && currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 && currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 && currentAction != Action.DEFEND) {
if (currentAction != Action.JUMP && currentAction != Action.FALL) {
changeAction(Action.WALK);
}
if (currentAction != Action.ATTACK1 &&
currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 &&
currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 &&
currentAction != Action.DEFEND &&
currentAction != Action.JUMP &&
currentAction != Action.FALL) {
changeAction(Action.WALK);
}
}
// 可以在空中攻击
/**
* 空中也可以攻击
*/
@Override
protected boolean canAttack() {
return super.canAttack() || currentAction == Action.JUMP || currentAction == Action.FALL;
}
/**
* 获取当前生命值
*/
public int getHp() {
return health;
}
}

View File

@@ -1,117 +0,0 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.Fighter;
public class AliceAnimationTest extends ApplicationAdapter {
private SpriteBatch batch;
private OrthographicCamera camera;
private Viewport viewport;
private Alice alice;
private float stateTimer; // 用于切换动作测试
@Override
public void create() {
// 初始化相机和批处理
camera = new OrthographicCamera();
viewport = new FitViewport(800, 600, camera);
batch = new SpriteBatch();
// 创建 Alice 实例
alice = new Alice();
// 初始位置
alice.getHitbox().setPosition(
viewport.getWorldWidth() / 2 - alice.getHitbox().width / 2,
viewport.getWorldHeight() / 2 - alice.getHitbox().height / 2
);
stateTimer = 0;
}
@Override
public void render() {
// 清屏
Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// 更新状态时间
float delta = Gdx.graphics.getDeltaTime();
stateTimer += delta;
// 控制 Alice 执行不同动作,测试动画切换
controlAliceActions();
// 更新 Alice
alice.update(delta);
// 绘制
batch.setProjectionMatrix(camera.combined);
batch.begin();
alice.render(batch);
batch.end();
}
/**
* 自动切换 Alice 动作,测试各种动画
*/
private void controlAliceActions() {
// 每2秒切换一个动作
if (stateTimer < 2) {
alice.changeAction(Fighter.Action.IDLE);
} else if (stateTimer < 4) {
alice.move(1, Gdx.graphics.getDeltaTime()); // 向右走
} else if (stateTimer < 6) {
alice.changeAction(Fighter.Action.JUMP);
} else if (stateTimer < 8) {
alice.attack(1); // 普通攻击1
} else if (stateTimer < 10) {
alice.attack(2); // 普通攻击2
} else if (stateTimer < 12) {
alice.attack(3); // 普通攻击3
} else if (stateTimer < 14) {
alice.takeHit(10); // 受击
} else {
// 循环
stateTimer = 0;
alice.getHitbox().setPosition(
viewport.getWorldWidth() / 2 - alice.getHitbox().width / 2,
viewport.getWorldHeight() / 2 - alice.getHitbox().height / 2
);
}
}
@Override
public void resize(int width, int height) {
viewport.update(width, height);
}
@Override
public void dispose() {
batch.dispose();
alice.dispose();
}
// 直接运行这个 main 方法即可启动测试
// public static void main(String[] args) {
// Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
// config.setTitle("Alice Animation Test");
// config.setWindowedMode(800, 600);
// config.setForegroundFPS(60);
// config.useVsync(true);
// new Lwjgl3Application(new AliceAnimationTest(), config);
// }
}

View File

@@ -11,10 +11,13 @@ import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Disposable;
import java.util.EnumMap;
/**
* 格斗角色父类,封装所有角色共有的动画和状态管理逻辑
*/
public abstract class Fighter implements Disposable {
public enum Action {
IDLE, WALK, JUMP, FALL,
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
@@ -22,328 +25,198 @@ public abstract class Fighter implements Disposable {
SPECIAL1, SPECIAL2,
DEATH
}
protected String name;
// 画帧间隔(秒)
protected static final float DEFAULT_FRAME_DURATION = 0.1f;
protected float[] frameDurations;
protected static final int DEFAULT_HEALTH = 100;
protected static final float DEFAULT_SPEED = 300f;
// 当前状态
protected Action currentAction;
protected float stateTime;
protected boolean isFacingRight;
protected boolean isAnimationFinished;
protected String name;
protected Action currentAction = Action.IDLE;
protected float stateTime = 0f;
protected boolean isFacingRight = true;
protected boolean isAnimationFinished = false;
// 动画集合
protected Animation<TextureRegion>[] animations;
protected EnumMap<Action, Animation<TextureRegion>> animations = new EnumMap<>(Action.class);
protected EnumMap<Action, Float> frameDurations = new EnumMap<>(Action.class);
// 碰撞检测
protected Rectangle hitbox;
protected Rectangle attackbox;
protected Rectangle hitbox = new Rectangle(0, 0, 64, 128);
protected Rectangle attackbox = new Rectangle(0, 0, 80, 80);
// 角色属性
protected float speed;
protected int health;
protected int maxHealth;
protected int attackPower;
protected float speed = DEFAULT_SPEED;
protected int health = DEFAULT_HEALTH;
protected int maxHealth = DEFAULT_HEALTH;
protected int attackPower = 10;
// 精灵图表
protected TextureAtlas atlas;
// 缩放比例
protected float scaleX = 1.0f;
protected float scaleY = 1.0f;
@SuppressWarnings("unchecked")
public Fighter() {
}
public Fighter(TextureAtlas atlas) {
this.atlas = atlas;
int actionCount = Action.values().length;
animations = new Animation[actionCount];
frameDurations = new float[actionCount];
for (int i = 0; i < actionCount; i++) {
frameDurations[i] = DEFAULT_FRAME_DURATION;
for (Action action : Action.values()) {
frameDurations.put(action, DEFAULT_FRAME_DURATION);
}
hitbox = new Rectangle(0, 0, 64, 128);
attackbox = new Rectangle(0, 0, 80, 80);
speed = 300f;
maxHealth = 100;
health = maxHealth;
attackPower = 10;
isFacingRight = true;
currentAction = Action.IDLE;
stateTime = 0;
isAnimationFinished = false;
loadAnimations();
}
/**
* 加载角色动画,由子类实现具体的动画帧加载
*/
protected abstract void loadAnimations();
/**
* 从精灵图表加载动画
*
* @param action 动作类型
* @param regionPrefix 该动作在图表中的区域前缀
* @param frameCount 帧数
* @param loop 是否循环
*/
protected void loadAnimationFromAtlas(Action action, String regionPrefix,
int frameCount, boolean loop) {
if (atlas == null) {
protected void loadAnimationFromAtlas(Action action, String regionPrefix, int frameCount, boolean loop) {
if (atlas == null)
throw new IllegalStateException("TextureAtlas 未初始化!");
}
if (frameCount <= 0) {
if (frameCount <= 0)
throw new IllegalArgumentException("帧数必须大于0: " + frameCount);
}
if (frameDurations == null || frameDurations.length <= action.ordinal()) {
throw new IllegalStateException("frameDurations 未初始化或大小不足");
}
Array<TextureRegion> frames = new Array<>();
for (int i = 0; i < frameCount; i++) {
String formattedIndex = SimpleFormatter.addLeadingZeros(i, 3);
String regionName = regionPrefix + formattedIndex;
String regionName = regionPrefix + SimpleFormatter.addLeadingZeros(i, 3);
TextureRegion region = atlas.findRegion(regionName);
if (region == null) {
throw new IllegalArgumentException("精灵图表中未找到区域: " + regionName +
" (前缀: " + regionPrefix + ", 索引: " + i + ")");
throw new IllegalArgumentException("未找到区域: " + regionName);
}
frames.add(region);
}
Animation<TextureRegion> animation = new Animation<>(
frameDurations[action.ordinal()],
frames);
Animation<TextureRegion> animation = new Animation<>(frameDurations.get(action), frames);
animation.setPlayMode(loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL);
animations[action.ordinal()] = animation;
animations.put(action, animation);
}
/**
* 为特定动作设置帧间隔
*/
protected void setFrameDuration(Action action, float duration) {
frameDurations[action.ordinal()] = duration;
if (animations[action.ordinal()] != null) {
animations[action.ordinal()].setFrameDuration(duration);
}
frameDurations.put(action, duration);
Animation<TextureRegion> anim = animations.get(action);
if (anim != null)
anim.setFrameDuration(duration);
}
/**
* 更新角色状态
*/
public void update(float deltaTime) {
stateTime += deltaTime;
isAnimationFinished = animations[currentAction.ordinal()].isAnimationFinished(stateTime);
Animation<TextureRegion> anim = animations.get(currentAction);
if (anim != null) {
isAnimationFinished = anim.isAnimationFinished(stateTime);
}
handleAnimationTransitions();
updateHitboxes();
}
/**
* 处理动画完成后的状态转换,子类可以重写以实现特定逻辑
*/
protected void handleAnimationTransitions() {
Animation<TextureRegion> currentAnim = animations[currentAction.ordinal()];
if (!isAnimationFinished)
return;
if (currentAnim.getPlayMode() != Animation.PlayMode.LOOP && isAnimationFinished) {
switch (currentAction) {
case ATTACK1:
case ATTACK2:
case ATTACK3:
case SPECIAL1:
case SPECIAL2:
changeAction(Action.IDLE);
break;
case HIT:
changeAction(Action.IDLE);
break;
case JUMP:
changeAction(Action.FALL);
break;
}
switch (currentAction) {
case ATTACK1:
case ATTACK2:
case ATTACK3:
case SPECIAL1:
case SPECIAL2:
case HIT:
changeAction(Action.IDLE);
break;
case JUMP:
changeAction(Action.FALL);
break;
default:
break;
}
}
/**
* 绘制角色
*/
public void render(SpriteBatch batch) {
Animation<TextureRegion> currentAnimation = animations[currentAction.ordinal()];
if (currentAnimation == null) {
Animation<TextureRegion> anim = animations.get(currentAction);
if (anim == null) {
Gdx.app.error("Fighter", "动画未初始化: " + currentAction);
return;
}
boolean loop = currentAnimation.getPlayMode() == Animation.PlayMode.LOOP;
TextureRegion currentFrame = currentAnimation.getKeyFrame(stateTime, loop);
if (currentFrame == null) {
TextureRegion frame = anim.getKeyFrame(stateTime, anim.getPlayMode() == Animation.PlayMode.LOOP);
if (frame == null) {
Gdx.app.error("Fighter", "动画帧为空: " + currentAction);
return;
}
// 1. 计算缩放后的帧尺寸(保持原始比例)
float frameWidth = currentFrame.getRegionWidth() * scaleX;
float frameHeight = currentFrame.getRegionHeight() * scaleY;
float frameWidth = frame.getRegionWidth() * scaleX;
float frameHeight = frame.getRegionHeight() * scaleY;
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2;
float drawY = hitbox.y;
// 2. 计算绘制位置始终以hitbox为基准水平居中、底部对齐关键
// 无论是否翻转x/y坐标都基于hitbox计算保证位置锚点一致
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2; // 水平居中hitbox中心和帧中心对齐
float drawY = hitbox.y; // 底部对齐hitbox底部和帧底部对齐
boolean wasFlippedX = frame.isFlipX();
frame.flip(!isFacingRight && !wasFlippedX, false);
frame.flip(isFacingRight && wasFlippedX, false);
// 3. 处理翻转用TextureRegion的flip方法避免手动偏移x坐标
// 先记录原始flip状态防止影响其他地方复用该帧
boolean wasFlippedX = currentFrame.isFlipX();
// 根据朝向设置翻转只翻转X轴Y轴不变
currentFrame.flip(!isFacingRight && !wasFlippedX, false); // 正向→不翻,反向→翻
currentFrame.flip(isFacingRight && wasFlippedX, false); // 修复原始已翻转的情况
// 4. 绘制:缩放中心为帧的中心,确保翻转/旋转时围绕自身中心
batch.draw(
currentFrame,
drawX, // 绘制X基于hitbox的居中位置固定不变
drawY, // 绘制Y基于hitbox的底部固定不变
frameWidth / 2, // 缩放/旋转中心X帧的中心
frameHeight / 2, // 缩放/旋转中心Y帧的中心
frameWidth, // 缩放后的宽度已乘scaleX
frameHeight, // 缩放后的高度已乘scaleY
1f, // X轴额外缩放这里已提前计算设为1避免重复缩放
1f, // Y轴额外缩放同上
0f // 旋转角度
);
// 5. 恢复帧的原始flip状态关键避免影响后续绘制
currentFrame.flip(wasFlippedX != currentFrame.isFlipX(), false);
batch.draw(frame, drawX, drawY, frameWidth / 2, frameHeight / 2, frameWidth, frameHeight, 1f, 1f, 0f);
frame.flip(wasFlippedX != frame.isFlipX(), false);
}
/**
* 改变角色动作
*/
public boolean changeAction(Action newAction) {
if (isActionUninterruptible(currentAction)) {
if (isActionUninterruptible(currentAction))
return false;
}
if (currentAction != newAction) {
currentAction = newAction;
stateTime = 0;
stateTime = 0f;
isAnimationFinished = false;
return true;
}
return false;
}
/**
* 判断动作是否不可被打断
*/
protected boolean isActionUninterruptible(Action action) {
return action == Action.HIT || action == Action.DEATH;
}
/**
* 更新碰撞框位置
*/
protected void updateHitboxes() {
if (isFacingRight) {
attackbox.setPosition(hitbox.x + hitbox.width - 10, hitbox.y + 20);
} else {
attackbox.setPosition(hitbox.x - attackbox.width + 10, hitbox.y + 20);
}
float offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + 20);
}
/**
* 处理角色移动
*/
public void move(float x, float deltaTime) {
if (x != 0) {
isFacingRight = x > 0;
hitbox.x += x * speed * deltaTime;
handleMoveState();
} else if (currentAction == Action.WALK) {
changeAction(Action.IDLE);
}
}
/**
* 处理移动状态转换,子类可重写
*/
protected void handleMoveState() {
if (currentAction != Action.ATTACK1 && currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 && currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 && currentAction != Action.JUMP &&
currentAction != Action.FALL && currentAction != Action.DEFEND) {
if (!isActionUninterruptible(currentAction) &&
currentAction != Action.JUMP &&
currentAction != Action.FALL &&
currentAction != Action.DEFEND &&
!currentAction.name().startsWith("ATTACK") &&
!currentAction.name().startsWith("SPECIAL")) {
changeAction(Action.WALK);
}
}
/**
* 执行攻击
*/
public boolean attack(int attackType) {
if (!canAttack()) {
if (!canAttack())
return false;
}
Action attackAction;
switch (attackType) {
case 1:
attackAction = Action.ATTACK1;
break;
case 2:
attackAction = Action.ATTACK2;
break;
case 3:
attackAction = Action.ATTACK3;
break;
case 4:
attackAction = Action.SPECIAL1;
break;
case 5:
attackAction = Action.SPECIAL2;
break;
default:
return false;
}
Action attackAction = switch (attackType) {
case 1 -> Action.ATTACK1;
case 2 -> Action.ATTACK2;
case 3 -> Action.ATTACK3;
case 4 -> Action.SPECIAL1;
case 5 -> Action.SPECIAL2;
default -> null;
};
return changeAction(attackAction);
return attackAction != null && changeAction(attackAction);
}
/**
* 判断是否可以攻击,子类可重写以实现特定逻辑
*/
protected boolean canAttack() {
return currentAction == Action.IDLE || currentAction == Action.WALK;
}
/**
* 处理受击
*/
public void takeHit(int damage) {
if (currentAction != Action.DEATH) {
health -= damage;
if (health <= 0) {
health = 0;
changeAction(Action.DEATH);
} else {
changeAction(Action.HIT);
}
}
if (currentAction == Action.DEATH)
return;
health = Math.max(0, health - damage);
changeAction(health == 0 ? Action.DEATH : Action.HIT);
}
public Rectangle getHitbox() {
@@ -362,21 +235,29 @@ public abstract class Fighter implements Disposable {
return health;
}
public int getMaxHealth() {
return maxHealth;
@Override
public void dispose() {
if (atlas != null)
atlas.dispose();
}
public Action getCurrentAction() {
return currentAction;
}
public int getAttackPower() {
return attackPower;
public float getX() {
return hitbox.x;
}
public String getName(){ return ""; }
public float getY() {
return hitbox.y;
}
@Override
public void dispose() {
public float getCenterX() {
return hitbox.x + hitbox.width / 2;
}
public float getCenterY() {
return hitbox.y + hitbox.height / 2;
}
}

View File

@@ -0,0 +1,70 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
public class Reimu extends Fighter {
public Reimu() {
super(new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\reimu\\reimu.atlas")));
// 设置角色属性
speed = 350f; // 更快的移动速度
maxHealth = 90; // 较低的生命值
health = maxHealth;
attackPower = 12; // 中等攻击力
}
@Override
protected void loadAnimations() {
// TODO Auto-generated method stub
// 加载基础动作动画
loadAnimationFromAtlas(Action.IDLE, "other/stand", 9, true);
loadAnimationFromAtlas(Action.WALK, "other/walkFront", 9, true);
loadAnimationFromAtlas(Action.JUMP, "other/jump", 8, false);
loadAnimationFromAtlas(Action.FALL, "other/hitSpin", 5, false);
// 加载攻击动作动画
loadAnimationFromAtlas(Action.ATTACK1, "attackAa/attackAa", 6, false);
loadAnimationFromAtlas(Action.ATTACK2, "attackAb/attackAb", 6, false);
loadAnimationFromAtlas(Action.ATTACK3, "attackAc/attackAc", 6, false);
loadAnimationFromAtlas(Action.ATTACK4, "attackAd/attackAd", 6, false);
// 加载受击动画
loadAnimationFromAtlas(Action.HIT, "hitSpin/hitSpin", 5, false);
// 设置帧间隔(动作速度)
setFrameDuration(Action.IDLE, 0.04f);
setFrameDuration(Action.WALK, 0.08f);
setFrameDuration(Action.ATTACK1, 0.07f);
setFrameDuration(Action.SPECIAL2, 0.06f);
}
@Override
protected void handleMoveState() {
if (currentAction != Action.ATTACK1 &&
currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 &&
currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 &&
currentAction != Action.DEFEND &&
currentAction != Action.JUMP &&
currentAction != Action.FALL) {
changeAction(Action.WALK);
}
}
/**
* 空中也可以攻击
*/
@Override
protected boolean canAttack() {
return super.canAttack() || currentAction == Action.JUMP || currentAction == Action.FALL;
}
/**
* 获取当前生命值
*/
public int getHp() {
return health;
}
}

View File

@@ -0,0 +1,183 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.Fighter;
import uno.mloluyu.characters.Reimu;
import uno.mloluyu.util.ClearScreen;
import java.util.Arrays;
import java.util.List;
import static uno.mloluyu.util.Font.loadChineseFont;
public class CharacterSelectScreen extends ScreenAdapter {
private boolean multiplayerMode = false; // 默认为单人模式
private final MainGame game;
private SpriteBatch batch;
private BitmapFont font;
private ShapeRenderer shapeRenderer;
private final List<String> characters = Arrays.asList("Alice", "Reimu", "暂定");
private int selectedIndex = -1;
private static final int BUTTON_WIDTH = 300;
private static final int BUTTON_HEIGHT = 80;
private static final int BUTTON_X = 800;
private static final int CONFIRM_Y = 200;
private static final int BACK_Y = 100;
public CharacterSelectScreen(MainGame game) {
this.game = game;
}
public void setMultiplayerMode(boolean multiplayerMode) {
this.multiplayerMode = multiplayerMode;
}
@Override
public void show() {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
font = loadChineseFont();
font.setColor(Color.WHITE);
font.getData().setScale(2f);
}
@Override
public void render(float delta) {
new ClearScreen();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
renderCharacters(mouseX, mouseY);
renderButtons(mouseX, mouseY);
renderTexts();
handleInput(mouseX, mouseY);
if (multiplayerMode) {
// 显示联机模式提示
batch.begin();
font.draw(batch, "联机模式 - 等待其他玩家连接...", 100, 100);
batch.end();
} else {
// 显示单人模式提示
batch.begin();
font.draw(batch, "单人模式", 100, 100);
batch.end();
}
}
private void renderCharacters(int mouseX, int mouseY) {
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
for (int i = 0; i < characters.size(); i++) {
int x = 200;
int y = 500 - i * 120;
boolean hovered = isHovered(mouseX, mouseY, x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
shapeRenderer.setColor(selectedIndex == i ? Color.GREEN : (hovered ? Color.LIGHT_GRAY : Color.DARK_GRAY));
shapeRenderer.rect(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
}
shapeRenderer.end();
}
private void renderButtons(int mouseX, int mouseY) {
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
drawButton(CONFIRM_Y, mouseX, mouseY, "确认");
drawButton(BACK_Y, mouseX, mouseY, "返回");
shapeRenderer.end();
}
private void renderTexts() {
batch.begin();
font.draw(batch, "选择你的角色", 200, 650);
for (int i = 0; i < characters.size(); i++) {
int x = 200 + 30;
int y = 500 - i * 120 + 50;
font.draw(batch, characters.get(i), x, y);
}
if (selectedIndex != -1) {
font.draw(batch, "已选择: " + characters.get(selectedIndex), 200, 100);
}
drawButtonText(CONFIRM_Y, "确认");
drawButtonText(BACK_Y, "返回");
batch.end();
}
private void drawButton(int y, int mouseX, int mouseY, String label) {
boolean hovered = isHovered(mouseX, mouseY, BUTTON_X, y, BUTTON_WIDTH, BUTTON_HEIGHT);
shapeRenderer.setColor(hovered ? Color.LIGHT_GRAY : Color.DARK_GRAY);
shapeRenderer.rect(BUTTON_X, y, BUTTON_WIDTH, BUTTON_HEIGHT);
}
private void drawButtonText(int y, String text) {
float textX = BUTTON_X + BUTTON_WIDTH / 2f - font.getScaleX() * text.length() * 10;
float textY = y + BUTTON_HEIGHT / 2f + 20;
font.draw(batch, text, textX, textY);
}
private boolean isHovered(int x, int y, int bx, int by, int bw, int bh) {
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
}
private void handleInput(int mouseX, int mouseY) {
if (Gdx.input.justTouched()) {
// 检查是否点击了角色按钮
for (int i = 0; i < characters.size(); i++) {
int x = 200;
int y = 500 - i * 120;
if (isHovered(mouseX, mouseY, x, y, BUTTON_WIDTH, BUTTON_HEIGHT)) {
selectedIndex = i;
Gdx.app.log("Character", "选择了角色: " + characters.get(i));
return; // 防止同时触发其他按钮
}
}
// 点击确认按钮
if (isHovered(mouseX, mouseY, BUTTON_X, CONFIRM_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) {
if (selectedIndex != -1) {
String selectedCharacter = characters.get(selectedIndex);
Gdx.app.log("Character", "确认角色: " + selectedCharacter);
Fighter fighter = null;
switch (selectedCharacter) {
case "Alice":
fighter = new Alice();
break;
case "Reimu":
fighter = new Reimu();
break;
// case "弓箭手":
// fighter = new Archer();
// break;
}
if (fighter != null) {
game.setScreen(new GameScreen(game, fighter));
}
}
}
// 点击返回按钮
if (isHovered(mouseX, mouseY, BUTTON_X, BACK_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) {
Gdx.app.log("Character", "返回主菜单");
game.setScreen(new MainMenuScreen(game));
}
}
}
@Override
public void dispose() {
batch.dispose();
font.dispose();
shapeRenderer.dispose();
}
}

View File

@@ -0,0 +1,23 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
/**
* Desktop 平台启动器
*/
public class DesktopLauncher {
public static void main(String[] args) {
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
float scale = 1.0F;
config.width = (int) (1920 * scale); // 窗口宽度
config.height = (int) (1080 * scale); // 窗口高度
config.resizable = false; // 窗口设置为大小不可改变
new LwjglApplication(new MainGame(), config);
}
}

View File

@@ -1,66 +0,0 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Scaling;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.ScalingViewport;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.FighterList;
public class GameCore implements ApplicationListener {
private SpriteBatch batch;
private Viewport viewport;
private Gaming gaming;
private Texture texture;
@Override
public void create() {
viewport = new ScalingViewport(Scaling.none, Launcher.width, Launcher.width);
texture = new Texture(Gdx.files.internal("src\\main\\resources\\backgrounds\\bg.png"));
batch = new SpriteBatch();
gaming = new Gaming(new Alice(), new Alice());
gaming.create();
}
@Override
public void render() {
Gdx.gl.glClearColor(150, 150, 150, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
viewport.apply();
batch.begin();
batch.draw(texture, 0, 0);
// alice1.update(Gdx.graphics.getDeltaTime());
// batch.begin();
// alice1.render(batch);
// batch.end();
gaming.render();
batch.end();
}
@Override
public void dispose() {
gaming.dispose();
}
@Override
public void resize(int width, int height) {
// 应用新的视口设置
viewport.update(width, height, true); // 第三个参数 true 表示相机居中
}
@Override
public void pause() {
}
@Override
public void resume() {
}
}

View File

@@ -0,0 +1,89 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.characters.Fighter;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen;
public class GameScreen extends ScreenAdapter {
private final MainGame game;
private final Fighter player;
private SpriteBatch batch;
private ShapeRenderer shapeRenderer;
public GameScreen(MainGame game, Fighter player) {
this.game = game;
this.player = player;
}
@Override
public void show() {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
}
@Override
public void render(float delta) {
new ClearScreen();
// 更新角色状态
player.update(delta);
// 发送本机玩家位置
if (NetworkManager.getInstance().isConnected()) {
NetworkManager.getInstance().sendPosition(player.getX(), player.getY());
}
// 渲染角色
batch.begin();
player.render(batch);
// 渲染其他玩家位置(联机模式)
if (NetworkManager.getInstance().isConnected()) {
renderOtherPlayers();
}
batch.end();
}
private void renderOtherPlayers() {
// 获取其他玩家位置并渲染
for (String position : NetworkManager.getInstance().getOtherPlayerPositions()) {
String[] coords = position.split(",");
if (coords.length == 2) {
try {
float x = Float.parseFloat(coords[0]);
float y = Float.parseFloat(coords[1]);
// 使用形状渲染器绘制其他玩家标记
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(Color.RED);
shapeRenderer.circle(x, y, 20); // 红色圆圈表示其他玩家
shapeRenderer.end();
// 在批处理中绘制文字
batch.begin();
// 这里可以添加玩家名字显示
batch.end();
} catch (NumberFormatException e) {
Gdx.app.error("GameScreen", "解析玩家位置失败: " + position);
}
}
}
}
@Override
public void dispose() {
batch.dispose();
player.dispose();
shapeRenderer.dispose();
// 断开网络连接
NetworkManager.getInstance().disconnect();
}
}

View File

@@ -1,38 +0,0 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import uno.mloluyu.characters.Fighter;
public class Gaming {
private Fighter selfFighter;
private Fighter frontFighter;
private SpriteBatch batch;
public Gaming(Fighter selfFighter, Fighter frontFighter) {
this.selfFighter = selfFighter;
this.frontFighter = frontFighter;
}
public void create() {
batch = new SpriteBatch();
}
public void render() {
selfFighter.update(Gdx.graphics.getDeltaTime());
frontFighter.update(Gdx.graphics.getDeltaTime());
batch.begin();
selfFighter.render(batch);
selfFighter.render(batch);
batch.end();
}
public void dispose() {
selfFighter.dispose();
frontFighter.dispose();
}
}

View File

@@ -1,20 +0,0 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
public class Launcher {
public static final int width = 640;
public static final int height = 480;
public static void main(String[] args) {
Lwjgl3ApplicationConfiguration configuration = new Lwjgl3ApplicationConfiguration();
configuration.setTitle("Test Game");
configuration.setWindowedMode(width, height);
configuration.setForegroundFPS(60);
configuration.useVsync(true);
new Lwjgl3Application(new GameCore(), configuration);
}
}

View File

@@ -0,0 +1,41 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
public class MainGame extends Game {
public static final float WORLD_WIDTH = 1920;
public static final float WORLD_HEIGHT = 1080;
private StartScreen startScreen;
private MainMenuScreen mainMenuScreen;
@Override
public void create() {
startScreen = new StartScreen(this);
mainMenuScreen = new MainMenuScreen(this);
setScreen(new MainMenuScreen(this));
setScreen(startScreen);
}
public void showGameScreen() {
setScreen(mainMenuScreen);
if (startScreen != null) {
startScreen.dispose();
startScreen = null;
}
}
public void dispose() {
if (startScreen != null) {
startScreen.dispose();
startScreen = null;
}
if (mainMenuScreen != null) {
mainMenuScreen.dispose();
mainMenuScreen = null;
}
}
}

View File

@@ -0,0 +1,103 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.versatile.FighterController;
import static uno.mloluyu.util.Font.loadChineseFont;
public class MainMenuScreen extends ScreenAdapter {
private final MainGame game;
private SpriteBatch batch;
private BitmapFont font;
private ShapeRenderer shapeRenderer;
// 按钮区域
private final int buttonWidth = 400;
private final int buttonHeight = 80;
private final int buttonX = 760;
private final int startY = 600;
private final int settingsY = 480;
private final int networkY = 360;
public MainMenuScreen(MainGame game) {
this.game = game;
}
@Override
public void show() {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
font = loadChineseFont();
font.setColor(Color.WHITE);
font.getData().setScale(2f);
}
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0.75F, 1, 0.98F, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
// 绘制按钮背景
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
drawButtonBox(startY, "开始游戏", mouseX, mouseY);
drawButtonBox(settingsY, "设置", mouseX, mouseY);
drawButtonBox(networkY, "联网设置", mouseX, mouseY);
shapeRenderer.end();
// 绘制按钮文字
batch.begin();
drawButtonText(startY, "开始游戏");
drawButtonText(settingsY, "设置");
drawButtonText(networkY, "联网设置");
batch.end();
// 点击事件
if (Gdx.input.justTouched()) {
if (isTouched(mouseX, mouseY, buttonX, startY)) {
Gdx.app.log("Button", "开始游戏按钮被点击!");
game.setScreen(new CharacterSelectScreen(game));
} else if (isTouched(mouseX, mouseY, buttonX, settingsY)) {
Gdx.app.log("Button", "设置按钮被点击!");
} else if (isTouched(mouseX, mouseY, buttonX, networkY)) {
Gdx.app.log("Button", "联网设置按钮被点击!");
game.setScreen(new NetworkSettingsScreen(game));
}
}
}
private void drawButtonBox(int y, String label, int mouseX, int mouseY) {
boolean hovered = isTouched(mouseX, mouseY, buttonX, y);
shapeRenderer.setColor(hovered ? Color.LIGHT_GRAY : Color.DARK_GRAY);
shapeRenderer.rect(buttonX, y, buttonWidth, buttonHeight);
}
private void drawButtonText(int y, String text) {
float textWidth = font.getRegion().getRegionWidth(); // 粗略估算
float textX = buttonX + buttonWidth / 2f - text.length() * 20; // 居中估算
float textY = y + buttonHeight / 2f + 20;
font.draw(batch, text, textX, textY);
}
private boolean isTouched(int x, int y, int bx, int by) {
return x >= bx && x <= bx + buttonWidth && y >= by && y <= by + buttonHeight;
}
@Override
public void dispose() {
if (batch != null) batch.dispose();
if (font != null) font.dispose();
if (shapeRenderer != null) shapeRenderer.dispose();
}
}

View File

@@ -0,0 +1,143 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import uno.mloluyu.network.ConnectClient;
import uno.mloluyu.network.ConnectServer;
import static uno.mloluyu.util.Font.loadChineseFont;
import uno.mloluyu.util.ClearScreen;
public class NetworkSettingsScreen extends ScreenAdapter {
private final MainGame game;
private SpriteBatch batch;
private BitmapFont font;
private ShapeRenderer shapeRenderer;
private static final int BUTTON_WIDTH = 400;
private static final int BUTTON_HEIGHT = 80;
private static final int BUTTON_X = 760;
private static final int CREATE_ROOM_Y = 500;
private static final int JOIN_ROOM_Y = 380;
private static final int EXIT_Y = 260; // 退出按钮位置
public NetworkSettingsScreen(MainGame game) {
this.game = game;
}
@Override
public void show() {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
font = loadChineseFont();
font.setColor(Color.WHITE);
font.getData().setScale(2f);
}
@Override
public void render(float delta) {
new ClearScreen();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
renderButtons(mouseX, mouseY);
renderTexts();
handleInput(mouseX, mouseY);
}
private void renderButtons(int mouseX, int mouseY) {
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
drawButton(CREATE_ROOM_Y, mouseX, mouseY);
drawButton(JOIN_ROOM_Y, mouseX, mouseY);
drawButton(EXIT_Y, mouseX, mouseY); // 新增退出按钮
shapeRenderer.end();
}
private void renderTexts() {
batch.begin();
font.draw(batch, "联机设置", BUTTON_X + 100, 650);
drawButtonText(CREATE_ROOM_Y, "创建房间");
drawButtonText(JOIN_ROOM_Y, "加入房间");
drawButtonText(EXIT_Y, "返回"); // 新增退出按钮文字
batch.end();
}
private void handleInput(int mouseX, int mouseY) {
if (Gdx.input.justTouched()) {
if (isHovered(mouseX, mouseY, BUTTON_X, CREATE_ROOM_Y)) {
Gdx.app.log("Network", "创建房间按钮被点击!");
new Thread(new ConnectServer(11455)).start();
// 添加服务器启动提示信息
Gdx.app.log("Network", "服务器已启动,等待玩家连接...");
System.out.println("服务器已启动,等待玩家连接...");
// 创建联机模式标识并传递到角色选择界面
CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game);
characterSelectScreen.setMultiplayerMode(true); // 设置为联机模式
game.setScreen(characterSelectScreen);
} else if (isHovered(mouseX, mouseY, BUTTON_X, JOIN_ROOM_Y)) {
Gdx.app.log("Network", "加入房间按钮被点击!");
// 使用LibGDX的输入对话框避免AWT线程问题
Gdx.input.getTextInput(new com.badlogic.gdx.Input.TextInputListener() {
@Override
public void input(String ip) {
if (ip != null && !ip.trim().isEmpty()) {
new Thread(() -> new ConnectClient(ip.trim(), 11455)).start();
Gdx.app.log("Network", "正在连接到服务器 " + ip.trim() + "...");
System.out.println("正在连接到服务器 " + ip.trim() + "...");
// 使用postRunnable确保在主线程中执行屏幕切换
Gdx.app.postRunnable(() -> {
CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game);
characterSelectScreen.setMultiplayerMode(true);
game.setScreen(characterSelectScreen);
});
}
}
@Override
public void canceled() {
Gdx.app.log("Network", "用户取消输入 IP");
}
}, "请输入服务器 IP 地址", "", "加入房间");
} else if (isHovered(mouseX, mouseY, BUTTON_X, EXIT_Y)) {
Gdx.app.log("Network", "退出按钮被点击!");
game.setScreen(new MainMenuScreen(game)); // 或者 Gdx.app.exit(); 直接退出游戏
}
}
}
private void drawButton(int y, int mouseX, int mouseY) {
boolean hovered = isHovered(mouseX, mouseY, BUTTON_X, y);
shapeRenderer.setColor(hovered ? Color.LIGHT_GRAY : Color.DARK_GRAY);
shapeRenderer.rect(BUTTON_X, y, BUTTON_WIDTH, BUTTON_HEIGHT);
}
private void drawButtonText(int y, String text) {
float textX = BUTTON_X + BUTTON_WIDTH / 2f - font.getScaleX() * text.length() * 10;
float textY = y + BUTTON_HEIGHT / 2f + 20;
font.draw(batch, text, textX, textY);
}
private boolean isHovered(int x, int y, int bx, int by) {
return x >= bx && x <= bx + BUTTON_WIDTH && y >= by && y <= by + BUTTON_HEIGHT;
}
@Override
public void dispose() {
batch.dispose();
font.dispose();
shapeRenderer.dispose();
}
}

View File

@@ -0,0 +1,80 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
/**
* 启动屏幕类
* 显示游戏Logo并在3秒后切换到主菜单界面
*/
public class StartScreen implements Screen {
private MainGame mainGame;
private Texture logoTexture;
private com.badlogic.gdx.graphics.g2d.SpriteBatch batch;
private float deltaSum;
public StartScreen(MainGame mainGame) {
this.mainGame = mainGame;
logoTexture = new Texture(Gdx.files.internal("src\\main\\resources\\logo.png"));
batch = new com.badlogic.gdx.graphics.g2d.SpriteBatch();
}
@Override
public void show() {
deltaSum = 0;
}
@Override
public void render(float delta) {
deltaSum += delta;
if (deltaSum >= 3.0F) {
if (mainGame != null) {
mainGame.showGameScreen();
System.out.println("已经切换到主菜单");
return;
}
}
Gdx.gl.glClearColor(0.75F, 1, 0.98F, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
float x = (MainGame.WORLD_WIDTH - logoTexture.getWidth()) / 2f;
float y = (MainGame.WORLD_HEIGHT - logoTexture.getHeight()) / 2f;
batch.draw(logoTexture, x, y);
batch.end();
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void hide() {
}
@Override
public void dispose() {
if (batch != null) {
batch.dispose();
}
if (logoTexture != null) {
logoTexture.dispose();
}
}
}

View File

@@ -0,0 +1,122 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
/**
* 屏幕过渡效果类
* 用于在不同屏幕之间实现平滑的过渡动画
*/
public class TransitionScreen implements Screen {
private ShapeRenderer shapeRenderer;
private SpriteBatch batch;
private float transitionTime = 0;
private float totalTransitionTime = 0.5f; // 过渡时间为0.5秒
private Runnable targetScreenAction;
private TransitionType transitionType = TransitionType.FADE_OUT_FADE_IN;
/**
* 过渡类型枚举
*/
public enum TransitionType {
FADE_OUT_FADE_IN
}
public TransitionScreen() {
this.shapeRenderer = new ShapeRenderer();
this.batch = new SpriteBatch();
}
/**
* 设置目标屏幕的显示动作
*
* @param targetScreenAction 目标屏幕的显示动作
*/
public void setTargetScreen(Runnable targetScreenAction) {
this.targetScreenAction = targetScreenAction;
this.transitionTime = 0;
}
@Override
public void show() {
// 屏幕显示时的初始化操作
}
@Override
public void render(float delta) {
// 清屏
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
transitionTime += delta;
float progress = Math.min(transitionTime / totalTransitionTime, 1);
// 绘制过渡效果
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(0, 0, 0, getAlpha(progress));
shapeRenderer.rect(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
shapeRenderer.end();
// 当过渡动画完成时,执行目标屏幕的显示动作
if (progress >= 1 && targetScreenAction != null) {
targetScreenAction.run();
}
}
/**
* 根据过渡进度计算透明度
*
* @param progress 过渡进度0-1
* @return 透明度值0-1
*/
private float getAlpha(float progress) {
switch (transitionType) {
case FADE_OUT_FADE_IN:
// 前半段淡出透明度从0到1后半段淡入透明度从1到0
if (progress < 0.5) {
return progress * 2;
} else {
return (1 - progress) * 2;
}
default:
return 1;
}
}
@Override
public void resize(int width, int height) {
// 屏幕尺寸变化时的处理
}
@Override
public void pause() {
// 游戏暂停时的处理
}
@Override
public void resume() {
// 游戏恢复时的处理
}
@Override
public void hide() {
// 屏幕隐藏时的处理
}
@Override
public void dispose() {
// 释放资源
if (shapeRenderer != null) {
shapeRenderer.dispose();
shapeRenderer = null;
}
if (batch != null) {
batch.dispose();
batch = null;
}
}
}

View File

@@ -0,0 +1,77 @@
package uno.mloluyu.network;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Net;
import com.badlogic.gdx.net.Socket;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
/**
* 客户端连接类(实例方式)
*/
public class ConnectClient {
private Socket socket;
public ConnectClient(String ip, int port) {
try {
socket = Gdx.net.newClientSocket(Net.Protocol.TCP, ip, port, null);
Gdx.app.log("Client", "成功连接到服务器: " + ip + ":" + port);
// 启动接收线程
new Thread(this::receiveMessages).start();
// 示例:发送一条欢迎消息
sendMessage("你好,我是客户端玩家");
} catch (Exception 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;
String message = new String(buffer, 0, read, StandardCharsets.UTF_8);
Gdx.app.log("Client", "收到服务器消息: " + message);
// 处理位置消息
if (message.startsWith("POS:")) {
String positionData = message.substring(4);
// 通知网络管理器更新其他玩家位置
NetworkManager.getInstance().updatePlayerPosition(positionData);
}
}
} catch (Exception e) {
Gdx.app.error("Client", "接收消息异常: " + e.getMessage(), e);
} finally {
disconnect();
}
}
// ... 现有代码 ...
public void sendMessage(String message) {
try {
OutputStream out = socket.getOutputStream();
out.write(message.getBytes(StandardCharsets.UTF_8));
out.flush();
} catch (Exception e) {
Gdx.app.error("Client", "发送消息失败: " + e.getMessage(), e);
}
}
public void disconnect() {
if (socket != null) {
socket.dispose();
socket = null;
Gdx.app.log("Client", "已断开与服务器的连接");
}
}
}

View File

@@ -2,44 +2,161 @@ package uno.mloluyu.network;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Net;
import com.badlogic.gdx.net.ServerSocket;
import com.badlogic.gdx.net.Socket;
import uno.mloluyu.desktop.Gaming;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class ConnectServer {
private static Socket socket;
private static String host = "";
private static int port = 10800;
/**
* 支持两个玩家连接的服务器类
*/
public class ConnectServer implements Runnable {
private final int port;
private ServerSocket serverSocket;
private final List<Player> connectedPlayers = new ArrayList<>();
private static final int MAX_PLAYERS = 2;
public static void connectServer() {
new Thread(new Runnable() {
@Override
public void run() {
try {
socket = Gdx.net.newClientSocket(Net.Protocol.TCP, host, port, null);
OutputStream outputStream = socket.getOutputStream();//读取套接字的数据流
InputStream inputStream = socket.getInputStream();
//Gaming gaming = new Gaming(); //进入游戏界面
//gaming.render();
// 玩家内部类存储Socket和玩家名字
private static class Player {
Socket socket;
String playerName;
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
Player(Socket socket) {
this.socket = socket;
this.playerName = "玩家" + (int) (Math.random() * 1000); // 默认随机名字
}
}
});
String getPlayerName() {
return playerName;
}
} catch (Exception e) {
void setPlayerName(String name) {
this.playerName = name;
}
} finally {
if (socket != null) {
socket.dispose();
}
Socket getSocket() {
return socket;
}
}
public ConnectServer(int port) {
this.port = port;
}
@Override
public void run() {
serverSocket = Gdx.net.newServerSocket(Net.Protocol.TCP, port, null);
Gdx.app.log("Server", "服务器已启动,等待玩家连接...");
try {
while (connectedPlayers.size() < MAX_PLAYERS) {
Socket socket = serverSocket.accept(null);
Player player = new Player(socket);
connectedPlayers.add(player);
Gdx.app.log("Server", "玩家连接成功: " + socket.getRemoteAddress() + ",默认名字: " + player.getPlayerName());
new Thread(() -> handlePlayer(player)).start();
}
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) {
Gdx.app.error("Server", "连接异常: " + e.getMessage(), e);
}
}
// ... 现有代码 ...
private void handlePlayer(Player player) {
try {
byte[] buffer = new byte[1024];
while (true) {
int read = player.getSocket().getInputStream().read(buffer);
if (read == -1)
break;
String message = new String(buffer, 0, read);
Gdx.app.log("Server", "收到玩家" + player.getPlayerName() + "的消息: " + message);
// 处理设置玩家名字的消息
if (message.startsWith("SET_NAME:")) {
String newName = message.substring(8);
player.setPlayerName(newName);
Gdx.app.log("Server", "玩家名字已更新为: " + newName);
}
// 处理位置消息并广播
else if (message.startsWith("POS:")) {
String positionData = message.substring(4);
// 广播给其他玩家
broadcastToOtherPlayers(player, "POS:" + positionData);
}
}
});
} catch (Exception e) {
Gdx.app.error("Server", "玩家通信异常: " + e.getMessage(), e);
} finally {
player.getSocket().dispose();
connectedPlayers.remove(player);
Gdx.app.log("Server", "玩家" + player.getPlayerName() + "断开连接");
}
}
// 新增广播方法
private void broadcastToOtherPlayers(Player sender, String message) {
for (Player player : connectedPlayers) {
if (player != sender) {
try {
OutputStream out = player.getSocket().getOutputStream();
out.write(message.getBytes(java.nio.charset.StandardCharsets.UTF_8));
out.flush();
} catch (Exception 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() {
for (Player player : connectedPlayers) {
player.getSocket().dispose();
}
connectedPlayers.clear();
if (serverSocket != null) {
serverSocket.dispose();
serverSocket = null;
}
Gdx.app.log("Server", "服务器已关闭");
}
}

View File

@@ -1,45 +0,0 @@
package uno.mloluyu.network;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Net;
import com.badlogic.gdx.net.ServerSocket;
import com.badlogic.gdx.net.Socket;
/**
* @author mloluyu
*/
public class CreateServer {
//创建套接字
private static ServerSocket serverSocket;
private static Socket clientSocket;
public static void createServer(int port) {
//开启本地服务器
serverSocket = Gdx.net.newServerSocket(Net.Protocol.TCP, port, null);
//启用新线程处理
new Thread(new Runnable() {
@Override
public void run() {
try {
clientSocket = serverSocket.accept(null);
System.out.println("玩家连接"+clientSocket);
//连接上后在这里编写进入游戏界面的代码
} catch (Exception e) {
} finally {
if (clientSocket != null) {
clientSocket.dispose();
}
}
}
});
}
public void serverDispose() {
//关闭主机
if (serverSocket != null) {
serverSocket.dispose();
}
}
}

View File

@@ -0,0 +1,106 @@
package uno.mloluyu.network;
import com.badlogic.gdx.Gdx;
import java.util.ArrayList;
import java.util.List;
/**
* 网络管理器,协调服务器和客户端通信
*/
public class NetworkManager {
private static NetworkManager instance;
private ConnectServer server;
private ConnectClient client;
private boolean isHost = false;
private List<String> playerPositions = new ArrayList<>();
public static NetworkManager getInstance() {
if (instance == null) {
instance = new NetworkManager();
}
return instance;
}
/**
* 创建房间(作为主机)
*/
public void createRoom() {
isHost = true;
server = new ConnectServer(11455);
new Thread(server).start();
Gdx.app.log("Network", "房间创建成功,等待其他玩家加入...");
}
/**
* 加入房间(作为客户端)
*/
public void joinRoom(String ip) {
isHost = false;
client = new ConnectClient(ip, 11455);
Gdx.app.log("Network", "正在加入房间: " + ip);
}
/**
* 发送玩家位置信息
*/
public void sendPosition(float x, float y) {
if (isHost && server != null) {
// 主机直接广播位置
broadcastMessage("POS:" + x + "," + y);
} else if (client != null) {
// 客户端发送位置到服务器
client.sendMessage("POS:" + x + "," + y);
}
}
/**
* 广播消息(主机使用)
*/
private void broadcastMessage(String message) {
// 这里需要实现广播逻辑
Gdx.app.log("Network", "广播消息: " + message);
}
/**
* 获取其他玩家位置
*/
public List<String> getOtherPlayerPositions() {
return playerPositions;
}
/**
* 断开连接
*/
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() {
return isHost;
}
public boolean isConnected() {
return server != null || client != null;
}
public void updatePlayerPosition(String positionData) {
// 简单实现:直接存储位置数据
playerPositions.add(positionData);
// 限制位置列表大小,避免内存泄漏
if (playerPositions.size() > 10) {
playerPositions.remove(0);
}
Gdx.app.log("Network", "更新其他玩家位置: " + positionData);
}
}

View File

@@ -0,0 +1,11 @@
package uno.mloluyu.util;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
public class ClearScreen {
public ClearScreen() {
Gdx.gl.glClearColor(0.3F, 0.3F, 0.5F, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
}
}

View File

@@ -0,0 +1,31 @@
package uno.mloluyu.util;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator.FreeTypeFontParameter;
public class Font {
public static BitmapFont loadChineseFont() {
FreeTypeFontGenerator generator = null;
try {
generator = new FreeTypeFontGenerator(Gdx.files.internal("FLyouzichati-Regular-2.ttf")); // 你的中文字体路径
FreeTypeFontParameter parameter = new FreeTypeFontParameter();
parameter.size = 48;
parameter.color = Color.WHITE;
parameter.borderWidth = 1;
parameter.borderColor = Color.DARK_GRAY;
parameter.characters = "返回主菜单确认角色选择了角色人游戏加入联机模式 - 等待其他玩家连接...房间创建房间联机设置开始游戏设置联网中国abcdefghijklmnopqrstuvw暂定xyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return generator.generateFont(parameter);
} catch (Exception e) {
Gdx.app.error("Font Error", "加载中文字体失败: " + e.getMessage());
return new BitmapFont(); // 回退默认字体
} finally {
if (generator != null)
generator.dispose();
}
}
}

View File

@@ -0,0 +1,61 @@
package uno.mloluyu.versatile;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
/**
* 按钮资源管理类:统一管理按钮枚举和图集资源
*/
public class ButtonActions {
private static TextureAtlas atlas;
// 初始化图集建议在游戏启动或Screen加载时调用一次
public static void loadAtlas(String atlasPath) {
if (atlas == null) {
atlas = new TextureAtlas(Gdx.files.internal(atlasPath));
}
}
// 获取图集区域
public static TextureRegion getRegion(Button button) {
if (atlas == null) {
throw new IllegalStateException("按钮图集尚未加载,请先调用 ButtonActions.loadAtlas()");
}
TextureRegion region = atlas.findRegion(button.getRegionName());
if (region == null) {
throw new IllegalArgumentException("图集中未找到区域: " + button.getRegionName());
}
return region;
}
// 释放资源建议在Screen dispose时调用
public static void dispose() {
if (atlas != null) {
atlas.dispose();
atlas = null;
}
}
/**
* 按钮枚举:统一定义所有按钮区域名
*/
public enum Button {
CONFIRM("confirm"), // 确认按钮
BACK("back"), // 返回按钮
WARRIOR("warrior"), // 战士角色按钮
MAGE("mage"), // 法师角色按钮
ARCHER("archer"); // 弓箭手角色按钮
private final String regionName;
Button(String regionName) {
this.regionName = regionName;
}
public String getRegionName() {
return regionName;
}
}
}

View File

@@ -1,4 +1,4 @@
package uno.mloluyu.Controller;
package uno.mloluyu.versatile;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
@@ -21,6 +21,9 @@ public class FighterController extends InputAdapter {
public FighterController(Fighter fighter) {
this.fighter = fighter;
}
public FighterController() {
this.fighter = null;
}
/**
* 更新控制器状态和冷却时间
@@ -33,6 +36,7 @@ public class FighterController extends InputAdapter {
// 处理移动输入
handleMovement();
}
/**

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because it is too large Load Diff

BIN
src/main/resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,24 @@
uiskin.png
format: RGBA8888
filter: Nearest,Nearest
repeat: none
button-up
rotate: false
xy: 0, 0
size: 200, 60
split: 10, 10, 10, 10
orig: 200, 60
offset: 0, 0
button-down
rotate: false
xy: 0, 60
size: 200, 60
split: 10, 10, 10, 10
orig: 200, 60
offset: 0, 0
white
rotate: false
xy: 0, 120
size: 1, 1
orig: 1, 1
offset: 0, 0

View File

@@ -0,0 +1,46 @@
{
"com.badlogic.gdx.graphics.g2d.BitmapFont": {
"default-font": {
"file": "default.fnt"
}
},
"com.badlogic.gdx.graphics.Color": {
"black": {
"r": 0,
"g": 0,
"b": 0,
"a": 1
},
"white": {
"r": 1,
"g": 1,
"b": 1,
"a": 1
}
},
"com.badlogic.gdx.scenes.scene2d.ui.Skin$TintedDrawable": {
"dialogDim": {
"name": "white",
"color": {
"r": 0,
"g": 0,
"b": 0,
"a": 0.45
}
}
},
"com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle": {
"default": {
"up": "button-up",
"down": "button-down"
}
},
"com.badlogic.gdx.scenes.scene2d.ui.TextButton$TextButtonStyle": {
"default": {
"up": "button-up",
"down": "button-down",
"font": "default-font",
"fontColor": "black"
}
}
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because it is too large Load Diff

BIN
target/classes/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,24 @@
uiskin.png
format: RGBA8888
filter: Nearest,Nearest
repeat: none
button-up
rotate: false
xy: 0, 0
size: 200, 60
split: 10, 10, 10, 10
orig: 200, 60
offset: 0, 0
button-down
rotate: false
xy: 0, 60
size: 200, 60
split: 10, 10, 10, 10
orig: 200, 60
offset: 0, 0
white
rotate: false
xy: 0, 120
size: 1, 1
orig: 1, 1
offset: 0, 0

View File

@@ -0,0 +1,46 @@
{
"com.badlogic.gdx.graphics.g2d.BitmapFont": {
"default-font": {
"file": "default.fnt"
}
},
"com.badlogic.gdx.graphics.Color": {
"black": {
"r": 0,
"g": 0,
"b": 0,
"a": 1
},
"white": {
"r": 1,
"g": 1,
"b": 1,
"a": 1
}
},
"com.badlogic.gdx.scenes.scene2d.ui.Skin$TintedDrawable": {
"dialogDim": {
"name": "white",
"color": {
"r": 0,
"g": 0,
"b": 0,
"a": 0.45
}
}
},
"com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle": {
"default": {
"up": "button-up",
"down": "button-down"
}
},
"com.badlogic.gdx.scenes.scene2d.ui.TextButton$TextButtonStyle": {
"default": {
"up": "button-up",
"down": "button-down",
"font": "default-font",
"fontColor": "black"
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
artifactId=game
groupId=uno.mloluyu
version=1.0-SNAPSHOT

View File

@@ -0,0 +1,13 @@
uno\mloluyu\network\ConnectServer$1$1.class
uno\mloluyu\network\ConnectServer$1.class
uno\mloluyu\network\CreateServer$1.class
uno\mloluyu\characters\FighterList.class
uno\mloluyu\network\CreateServer.class
uno\mloluyu\desktop\Launcher.class
uno\mloluyu\characters\Fighter$Action.class
uno\mloluyu\desktop\GameCore.class
uno\mloluyu\characters\Fighter.class
uno\mloluyu\characters\Alice.class
uno\mloluyu\Controller\FighterController.class
uno\mloluyu\network\ConnectServer.class
uno\mloluyu\util\SimpleFormatter.class

View File

@@ -0,0 +1,10 @@
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\Alice.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\Fighter.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\FighterList.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\Controller\FighterController.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\GameCore.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\Launcher.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\MainMenuScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\network\ConnectServer.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\network\CreateServer.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\util\SimpleFormatter.java