請問有大大知道這個介面是甚麼檔案在管的嗎?
請問有大大知道這個小地圖 是甚麼檔案在管的嗎?希望有大大可以幫幫忙目前直有顯示玩家而已 我想新增怪物顯示到時候改好我會分享給大家
本帖最後由 滅龍猛虎紫電 於 2025-12-21 00:30 編輯
我只知道抽檔PNG可以帶地圖圖片
控制顯示可能在XML 沒碰過猜測 本帖最後由 軒云熊 於 2025-12-21 05:01 編輯
滅龍猛虎紫電 發表於 2025-12-21 00:14
我只知道抽檔PNG可以帶地圖圖片
控制顯示可能在XML 沒碰過猜測
我是說核心檔案部分希望有大大可以幫忙謝謝你的回覆 ^__^
如果有類似 DAT 解密工具就更好了 可以直接找它的邏輯 >"<
核心原理
实现动态怪物标识,需要完成一个数据流闭环:
服务端:实时计算玩家周围怪物的坐标。
通信:将怪物坐标列表通过网络封包发送给客户端。
客户端:接收封包,解析坐标,并将其转换为小地图上的像素坐标,动态绘制(如红点)。
具体实现方案(基于您拥有的源码)
这是一个高级修改方案,需要对游戏代码有一定的理解。
方案一:利用现有任务目标标记系统(推荐先尝试)
从您的截图看到,游戏本身已有“红色/蓝色/绿色怪物任務”的追踪功能。这是最理想的切入点。
目标:尝试将“所有可见怪物”临时标记为一种“可攻击的任务目标”。
操作:
在服务端源码中,找到处理“发送周围实体信息”给客户端的函数(通常与玩家视野更新相关)。
修改此函数,使其在发送给客户端的怪物数据封包中,为每个怪物添加一个特殊的“标记状态”标志(例如,模仿任务怪物的标志位)。
在客户端源码中,找到处理“小地图绘制实体”的函数。修改其逻辑,使其对带有“标记状态”的怪物,使用一个醒目的图标(如红点) 进行绘制,而非默认的普通点或不绘制。
优点:复用游戏原有渲染逻辑,稳定。
缺点:需要对网络封包结构和渲染逻辑有深入研究。
方案二:通过Lua脚本系统实现(如果服务端支持)
许多天堂私服核心支持Lua脚本扩展,这是最安全、最模块化的方式。
服务端(Lua脚本):
编写一个Lua脚本,定时(如每秒)获取指定玩家的位置,并查询其周围一定范围内的所有怪物。
将这些怪物的ID、坐标、类型等信息,通过游戏内建的自定义封包功能发送给对应的客户端。
客户端(Lua脚本或修改):
如果客户端支持Lua,则编写对应的接收脚本,解析封包,并在小地图的UI层上绘制图标。
如果客户端不支持,则需修改客户端源码,增加一个用于接收和解析此自定义封包的模块,并调用小地图绘制接口。
优点:无需修改核心源码,热加载,易于调试和管理。
缺点:需要服务器和客户端都支持Lua,且性能需优化(不能过于频繁发送)。
方案三:直接修改客户端与服务端通信(底层方法)
如果Lua不支持,则必须直接修改源码。
服务端修改:
定位到 Player 或 World 相关的类,找到 updateSurroundingObjects 或类似名称的函数。
在此函数中,除了发送怪物基础信息外,新增一个专门用于发送小地图标识的自定义封包。这个封包应尽量简洁,只包含怪物坐标和基础类型。
客户端修改:
在客户端网络封包处理模块中,注册并解析这个新的自定义封包。
将解析到的坐标数据,传递给小地图UI模块。
修改小地图的绘制函数(通常在一个单独的UI类中),使其在绘制地图背景和玩家自身位置后,遍历接收到的怪物坐标列表,在对应位置绘制一个小的红色位图(Sprite)。
坐标转换关键:必须正确将游戏世界坐标 (X, Y) 转换为小地图的屏幕坐标 (pixelX, pixelY)。转换公式通常在客户端已有,需要找到并复用。
给您的最直接建议
首先确认:检查您的服务器端和客户端核心是否支持Lua脚本扩展。如果支持,方案二是首选。
从查看开始:在您的服务器端源码中,搜索关键词如 SendNearbyObjects、spawn、NpcInfo 或 AbstractServerPacket,找到发送周围怪物数据给客户端的地方。这是您需要修改的起点。
绘制测试:在客户端源码中,搜索 Minimap、SmallMap、DrawMap 等类,先尝试硬编码一个静态的坐标点,看看能否成功在小地图上绘制一个红点。这是验证客户端绘制能力的关键一步。
.........................................................................................................................................
DAT文件解密与解析
DAT文件通常是游戏的资源封包,里面包含了图片、配置、模型等所有客户端资源。
工具建议:
通用工具:对于《天堂》这类游戏,可以尝试 GameDataViewer、L2FileEdit 或特定私服社区发布的解包工具。
自研工具:如果通用工具无效,可能需要根据文件头特征(如 0x00、0x11 等标识)自己编写解密脚本。常见方法是逆向主程序的 OpenFile、ReadFile 等函数,找到解密密钥和算法。
解密后:您会得到大量子文件,其中就包含地图相关的 PNG图片 和 XML/TXT配置。 爱吃鱼的大柚 發表於 2025-12-21 19:03
核心原理
实现动态怪物标识,需要完成一个数据流闭环:
請問大大可以告訴我玩家顯示在小地圖上的檔案名稱嗎? 也許那裏可以找到答案
MiniMapUI.xml
<?xml version="1.0"?>
<MiniMapUI Name="MiniMapUI">
<Window Name="Window" X="485" Y="475" Toggle="false" MostTop="1" Activate="1" Transparent="false">
<Image Name="Frame" X="4" Y="-9" Surf="3275;1"/>
<Image Name="Base" X="11" Y="20" Surf="3274" MostTop="1"/>
</Window>
</MiniMapUI>
既然您已经找到小地图的UI配置文件,可以尝试修改这个XML来添加一个“怪物标记层”,但这需要客户端代码支持:
尝试在XML中添加一个图层:
<?xml version="1.0"?>
<MiniMapUI Name="MiniMapUI">
<Window Name="Window" X="485" Y="475" Toggle="false" MostTop="1" Activate="1" Transparent="false">
<Image Name="Frame" X="4" Y="-9" Surf="3275;1"/>
<Image Name="Base" X="11" Y="20" Surf="3274" MostTop="1"/>
<!-- 尝试添加一个怪物标记层 -->
<Layer Name="MonsterMarkers" X="0" Y="0" Visible="true"/>
</Window>
</MiniMapUI>
但更实际的途径是:
在客户端源码中找到绘制小地图的函数
在此函数中,在绘制玩家自身图标后,添加遍历周围怪物并绘制红点的代码
需要从游戏世界对象中获取怪物坐标列表
具体查找步骤
搜索资源引用:
在客户端源码中全局搜索 "3274" 和 "3275"(Base和Frame的资源ID)。找到引用这些ID的代码,那里就是小地图的绘制起点。
从绘制函数入手:
一旦找到绘制小地图背景的代码,就在附近寻找绘制玩家图标的函数调用。
坐标转换是关键:
找到将游戏世界坐标 (x, y) 转换为小地图屏幕坐标 (screenX, screenY) 的函数。这个函数是您添加怪物标记时必须使用的。
1. 文件功能解析
**3274.img:
这是小地图的背景图片文件**。它就是您在游戏小地图窗口中看到的那张静态的、绘制了地形、道路和固定建筑的图片。通常,游戏中的每张大地图(如“歌唱之岛”、“古鲁丁村庄”)都会对应一个这样编号的 .img 文件。
**3274.til:
这是关键的地图数据索引文件**。.til 文件通常存储了地图的“块”(Tile)信息,它定义了游戏世界坐标 (X, Y) 与 3274.img 这张背景图片上像素位置之间的映射关系。简单来说,它是一张“转换表”,告诉游戏引擎:当玩家位于游戏世界中的某个坐标时,应该在小地图背景图片的哪个位置绘制代表玩家的箭头。
2. 它们如何与您的目标关联
您希望实现的“实时怪物标识”功能,其技术基础就建立在这个坐标转换上。要实现它,您需要:
理解坐标转换:您需要从游戏代码或 .til 文件的结构中,逆向推导出转换公式。公式大致是:小地图像素X = (游戏世界坐标X - 地图偏移X) * 缩放比例。.til 文件很可能就存储了“偏移量”和“缩放比例”等关键参数。
找到动态绘制层:即使您修改了 3274.img 这张背景图(例如直接在上面画上红点),它也是静态的,无法实现“实时”跟踪移动的怪物。您之前找到的 MiniMapUI.xml 只定义了静态的UI框架。真正的动态图标(玩家自身、队友、怪物)是在游戏引擎中,在渲染小地图背景(3274.img)之后,再实时绘制上去的。
3. 给您的具体操作建议
要实现怪物红点,您不能直接修改 3274.img,而应该:
解析 .til 文件:使用十六进制编辑器或编写一个小程序,尝试解析 3274.til 的文件格式,找到坐标转换所需的偏移量和缩放系数。这是最核心的一步。
定位绘制代码:在客户端源码中,搜索加载或使用 3274.img 和 3274.til 的代码。找到这个位置,您就找到了小地图引擎的入口。通常,在这段代码的后面,就会接着绘制玩家、NPC等动态图标的函数。
添加绘制逻辑:在绘制动态图标的代码段中,您需要插入新的逻辑:
从游戏内存或网络封包中,获取周围怪物的坐标列表。
利用从 .til 文件解析出的转换公式,将每个怪物的游戏坐标转换为小地图上的像素坐标。
调用游戏的2D绘图函数,在计算出的像素坐标上,绘制一个红色的点或小图标。
总结:3274.img 是小地图的“画布”,3274.til 是这张画布的“比例尺和坐标轴”。您的目标不是修改画布本身,而是要在游戏引擎使用这个画布和比例尺绘图时,让它多画一种红色的点。请集中精力在客户端源码中,寻找负责“使用 .til 文件转换坐标并绘制图标”的那个核心函数。
我搜一搜核心,搜到了回你 是不是可以从组队的地图显示队友入手? 本帖最後由 軒云熊 於 2025-12-22 23:47 編輯
爱吃鱼的大柚 發表於 2025-12-22 20:45
是不是可以从组队的地图显示队友入手?
謝謝 爱吃鱼的大柚 大大 MiniMapUI 測試過 確定是小地圖雷達框架
显示队友也可以如果能找到 玩家本身 顯示在小地圖上的 檔案名稱 就可以找到答案了
src\com\lineage\server\model\L1Party.java文件路径
文件内搜索
/**
* 該隊伍目前人數(同地圖)
* @return
*/
public int partyUserInMap(final short mapid) {
int i = 0;
if (this._membersList.isEmpty()) {
return 0;
}
if (this._membersList.size() <= 0) {
return 0;
}
for (final L1PcInstance tgpc : getMembers()) {
short tgpcMapid = tgpc.getMapId();
if (tgpcMapid == mapid) {
i += 1;
}
}
return i;
}
public List<L1PcInstance> getMemberList() {
return _membersList;
}
public final int checkMentor(final L1Apprentice apprentice) {
int checkType = 0;
for (final L1PcInstance member : _membersList) {
if (apprentice.getMaster().getId() == member.getId()) {
checkType += 4;
} else if (apprentice.getTotalList().contains(member)) {
checkType++;
}
}
return checkType;
}
/** 您要实现“实时标识当前地图的怪物”,在服务端需要编写一个极其相似的方法。以下是仿照此逻辑的伪代码:
/**
* 获取指定地图上的所有怪物列表
* @param mapId 地图ID
* @return 该地图上的怪物列表
*/
public List<L1NpcInstance> getMonstersInMap(final short mapId) {
List<L1NpcInstance> monstersInMap = new ArrayList<>();
// 1. 获取全服务器怪物列表(假设通过L1World获取)
for (final L1NpcInstance npc : L1World.getInstance().getVisibleNpc()) {
// 2. 筛选:是怪物(非NPC)且位于指定地图
if (npc instanceof L1MonsterInstance && npc.getMapId() == mapId) {
monstersInMap.add(npc);
}
}
return monstersInMap;
} 本帖最後由 軒云熊 於 2025-12-24 10:17 編輯
爱吃鱼的大柚 發表於 2025-12-23 07:43
您要实现“实时标识当前地图的怪物”,在服务端需要编写一个极其相似的方法。以下是仿照此逻辑的伪代码:
/ ...
位置在 com.lineage.server.clientpackets.C_LoginToServer;
getFocus()
/**
* 遊戲焦點及人物狀態更新
*
* @param pc
*/
private void getFocus(L1PcInstance pc) {//src014
try {
pc.set_showId(-1);// 重置副本編號
World.get().addVisibleObject(pc);// 將物件增加到MAP世界裡
if ((ConfigTurn.METE_LEVEL > 0) && (pc.getMeteLevel() > 0)) {// 轉生能力
pc.resetMeteAbility();
}
pc.resetPolyPower();
pc.sendPackets(new S_OwnCharStatus(pc));// 角色資訊
pc.sendPackets(new S_MapID(pc, pc.getMapId(), pc.getMap().isUnderwater()));// 更新角色所在的地圖
pc.sendPackets(new S_OwnCharPack(pc));// 物件封包(本身)
boolean isTimingmap = MapsTable.get().isTimingMap(pc.getMapId());// 傳送地圖是否為限時地圖
/** 加入限時地圖清單 */
if (isTimingmap) { // 是限時地圖
// 地圖限制時間(秒數)
final int maxMapUsetime = MapsTable.get().getMapTime(pc.getMapId()) * 60;
// 已使用秒數
int usedtime = pc.getMapUseTime(pc.getMapId());
// 剩餘時間(秒)
int leftTime = (maxMapUsetime - usedtime);
MapTimerThread.put(pc, leftTime);
// System.out.println("加入限時地圖清單");
} else if (MapTimerThread.TIMINGMAP.get(pc) != null) {// 在清單中
MapTimerThread.TIMINGMAP.remove(pc);// 移出清單
// System.out.println("移出清單");
}
ArrayList<L1PcInstance> otherPc = World.get().getVisiblePlayer(pc);
if (otherPc.size() > 0) {
for (L1PcInstance tg : otherPc) {
tg.sendPackets(new S_OtherCharPacks(pc));//// 物件封包(其他人物)
}
}
pc.setVipStatus();
} catch (Exception e) {
_log.error(e.getLocalizedMessage(), e);
}
}
這段 pc.sendPackets(new S_MapID(pc, pc.getMapId(), pc.getMap().isUnderwater()));// 更新角色所在的地圖
我ˋ試過 就是 把人物焦點放在小地圖和主畫面地圖上 它是綁在一起的
大大可以幫忙看一下嗎? 看看能否找到標記紅色三角形的片段可能在getFocus()這個方法的其中一個裡
應該有一個封包對應到小地圖 顯示紅色三角形 >"<
本帖最後由 爱吃鱼的大柚 於 2025-12-24 17:22 編輯
小地图相关逻辑深入分析
一、 S_MapID类中的小地图逻辑
1. 核心小地图功能
java
下载
复制
this.writeC(S_WORLD);
this.writeH(mapid); // 关键:写入地图ID,客户端据此加载对应的小地图
this.writeC(isUnderwater ? 0x01 : 0x00);// 水下状态,可能影响小地图显示
2. 地图ID处理流程
副本地图检测:
if (QuestMapTable.get().isQuestMap(pc.getMapId())) {
// 副本地图特殊处理
}
副本地图可能有独立的小地图配置
虚拟地图映射:
int newmapid = MapsTable.get().getCopyMapId(mapid);
if (newmapid >= 0) {
mapid = newmapid;// 使用映射后的地图ID
}
影响:虚拟地图可能共享或拥有独立的小地图资源
3. GM专用构造函数的特殊处理
this.writeC(S_WORLD);
this.writeH(mapid);
this.writeC(0x00);// 固定为非水下状态
// 额外写入固定字节序列
this.writeC(0x10);
this.writeH(0x00f8);
// ... 更多固定数据
GM移动可能绕过正常的小地图加载逻辑
固定字节可能包含特殊的小地图显示标志
二、 S_OwnCharPack类中的小地图逻辑
1. 角色状态对小地图的影响
if (pc.isInvisble() || pc.isGmInvis()) {
status |= STATUS_INVISIBLE;// 隐身状态,可能影响小地图上自身的显示
}
隐身状态下,角色在小地图上可能不可见
2. 移动状态相关标志
if (pc.isFastMovable()) {
status |= STATUS_FASTMOVABLE;// 加速状态,可能影响小地图移动动画
}
加速状态可能改变小地图上移动指针的显示效果
3. 坐标和方向信息
writeH(pc.getX());
writeH(pc.getY());
writeC(pc.getHeading());// 角色朝向,影响小地图箭头方向
这些是小地图定位的核心数据
4. 光照范围影响
writeC(pc.getOwnLightSize());// 自身光照范围
可能影响小地图的视野范围或迷雾显示
5. 血盟和队伍信息
writeD(pc.getClanid() > 0 ? pc.getClan().getEmblemId() : 0);// 盟徽
writeS(pc.getClanname());// 血盟名称
writeC(pc.getClanRank() > 0 ? pc.getClanRank() << 4 : 0xb0);// 阶级
if (pc.isInParty()) {
writeC(100 * pc.getCurrentHp() / pc.getMaxHp());// 队伍中显示血量百分比
} else {
writeC(255);// 非队伍成员
}
血盟信息可能影响小地图上的标识
队伍血量显示可能与小地图上的队友状态相关
三、小地图相关的重要发现
1. 地图切换的完整流程
复制
玩家移动 → S_MapID(新地图) → 客户端加载小地图资源 → S_OwnCharPack(更新位置)
2. 潜在的小地图问题点
问题1:副本地图的小地图同步
// 离开副本时重置showId,但地图ID可能已经改变
pc.set_showId(-1);
如果副本有特殊小地图,离开时可能需要额外清理
问题2:虚拟地图映射
int newmapid = MapsTable.get().getCopyMapId(mapid);
虚拟地图的小地图资源需要正确配置,否则可能显示错误
问题3:状态同步时机
隐身、加速等状态变化时,可能需要额外的小地图更新包
3. GM功能的特殊处理
GM构造函数中的固定字节序列可能包含强制小地图刷新的标志,但具体含义需要查看客户端代码确认。
四、建议的优化和改进
明确小地图状态标志
在状态位掩码中增加明确的小地图相关标志
如:STATUS_MINIMAP_VISIBLE等
小地图数据单独封装
考虑创建专门的S_MiniMapPack数据包
包含小地图需要的精简数据
状态变化时的主动通知
当影响小地图显示的状态改变时,主动发送更新包
总结
两个文件中小地图相关的逻辑主要体现为:
S_MapID:负责地图切换和小地图资源加载
S_OwnCharPack:负责角色在小地图上的状态和位置显示 这是一个实现怪物在小地图上显示为红点并跟随移动的模拟系统。基于您提供的游戏服务器结构,我创建了一个完整的客户端-服务器模拟,演示如何实现这一功能。
关键技术要点说明:
怪物分类显示系统:
主动攻击怪物显示为红点
被动怪物显示为黄点
BOSS级怪物显示为大红点并标注"BOSS"
实时位置同步机制:
服务器发送S_NPCPack封包通知怪物出现
服务器发送S_MoveNpcPacket封包更新怪物位置
客户端接收并立即更新小地图显示
小地图坐标转换:
将游戏世界坐标转换为小地图像素坐标
只显示玩家视野范围内的怪物
玩家固定在小地图中心,怪物位置相对显示
性能优化考虑:
使用CopyOnWriteArrayList保证线程安全
只更新视野内的怪物,减少绘制开销
根据怪物类型决定绘制细节(名称显示等)
扩展性设计:
可轻松添加更多怪物类型和图标
支持动态添加/移除怪物
可扩展为显示NPC、队友等其他实体import java.util.*;
import java.util.concurrent.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import java.util.List;
/**
* 怪物小地图红点显示系统模拟
* 演示如何在小地图上显示所有怪物为红点并实时跟随移动
*/
// 1. 怪物数据包类 - 发送怪物出现信息给客户端
class S_NPCPack {
private final int monsterId;
private final int x, y;
private final int gfxId;
private final int level;
private final boolean isAggressive;
private final int minimapType; // 0=不显示, 1=红点, 2=黄点, 3=BOSS
public S_NPCPack(Monster monster) {
this.monsterId = monster.getId();
this.x = monster.getX();
this.y = monster.getY();
this.gfxId = monster.getGfxId();
this.level = monster.getLevel();
this.isAggressive = monster.isAggressive();
this.minimapType = calculateMinimapType(monster);
}
private int calculateMinimapType(Monster monster) {
if (monster.isBoss()) {
return 3; // BOSS特殊图标
} else if (monster.isAggressive() || monster.getLevel() > 10) {
return 1; // 主动怪或高级怪显示红点
} else {
return 0; // 被动低级怪不显示
}
}
// 获取封包数据(模拟)
public byte[] getPacketData() {
// 实际游戏中会使用二进制协议
// 这里简化返回关键信息
System.out.println("发送怪物出现封包: ID=" + monsterId +
", 位置=(" + x + "," + y + ")" +
", 小地图类型=" + minimapType);
return new byte;
}
public int getMonsterId() { return monsterId; }
public int getX() { return x; }
public int getY() { return y; }
public int getMinimapType() { return minimapType; }
}
// 2. 怪物移动封包类 - 发送怪物移动信息给客户端
class S_MoveNpcPacket {
private final int monsterId;
private final int newX, newY;
private final int heading;
public S_MoveNpcPacket(Monster monster, int newX, int newY, int heading) {
this.monsterId = monster.getId();
this.newX = newX;
this.newY = newY;
this.heading = heading;
}
public byte[] getPacketData() {
System.out.println("发送怪物移动封包: ID=" + monsterId +
", 新位置=(" + newX + "," + newY + ")" +
", 方向=" + heading);
return new byte;
}
public int getMonsterId() { return monsterId; }
public int getNewX() { return newX; }
public int getNewY() { return newY; }
}
// 3. 怪物对象类
class Monster {
private static int nextId = 1;
private final int id;
private int x, y;
private int gfxId;
private int level;
private boolean aggressive;
private boolean boss;
private String name;
public Monster(int x, int y, String name, int level, boolean aggressive, boolean boss) {
this.id = nextId++;
this.x = x;
this.y = y;
this.name = name;
this.level = level;
this.aggressive = aggressive;
this.boss = boss;
this.gfxId = level * 10; // 模拟图形ID
}
public void move(int newX, int newY) {
this.x = newX;
this.y = newY;
}
public void randomMove(int mapWidth, int mapHeight) {
int dx = (int)(Math.random() * 21) - 10; // -10 到 10
int dy = (int)(Math.random() * 21) - 10;
int newX = Math.max(0, Math.min(mapWidth - 1, x + dx));
int newY = Math.max(0, Math.min(mapHeight - 1, y + dy));
move(newX, newY);
}
// Getters
public int getId() { return id; }
public int getX() { return x; }
public int getY() { return y; }
public int getGfxId() { return gfxId; }
public int getLevel() { return level; }
public boolean isAggressive() { return aggressive; }
public boolean isBoss() { return boss; }
public String getName() { return name; }
}
// 4. 游戏服务器类 - 管理怪物和发送封包
class GameServer {
private final List<Monster> monsters = new ArrayList<>();
private final List<Client> clients = new ArrayList<>();
private final int mapWidth = 5000;
private final int mapHeight = 5000;
// 添加怪物
public void addMonster(Monster monster) {
monsters.add(monster);
// 通知所有客户端怪物出现
broadcastToAllClients(new S_NPCPack(monster));
}
// 添加客户端
public void addClient(Client client) {
clients.add(client);
// 发送所有已有怪物信息给新客户端
for (Monster monster : monsters) {
client.receivePacket(new S_NPCPack(monster));
}
}
// 广播给所有客户端
private void broadcastToAllClients(Object packet) {
for (Client client : clients) {
client.receivePacket(packet);
}
}
// 更新所有怪物位置(模拟怪物移动)
public void updateMonsterPositions() {
for (Monster monster : monsters) {
int oldX = monster.getX();
int oldY = monster.getY();
// 随机移动怪物
monster.randomMove(mapWidth, mapHeight);
// 如果位置改变,通知客户端
if (monster.getX() != oldX || monster.getY() != oldY) {
int heading = calculateHeading(oldX, oldY, monster.getX(), monster.getY());
broadcastToAllClients(new S_MoveNpcPacket(monster,
monster.getX(), monster.getY(), heading));
}
}
}
// 计算移动方向
private int calculateHeading(int fromX, int fromY, int toX, int toY) {
double angle = Math.atan2(toY - fromY, toX - fromX);
double degree = Math.toDegrees(angle);
if (degree < 0) degree += 360;
// 转换为8方向
if (degree >= 337.5 || degree < 22.5) return 0; // 东
if (degree >= 22.5 && degree < 67.5) return 1; // 东北
if (degree >= 67.5 && degree < 112.5) return 2; // 北
if (degree >= 112.5 && degree < 157.5) return 3;// 西北
if (degree >= 157.5 && degree < 202.5) return 4;// 西
if (degree >= 202.5 && degree < 247.5) return 5;// 西南
if (degree >= 247.5 && degree < 292.5) return 6;// 南
return 7; // 东南
}
public List<Monster> getMonsters() { return monsters; }
public int getMapWidth() { return mapWidth; }
public int getMapHeight() { return mapHeight; }
}
// 5. 小地图显示类(客户端)
class MiniMapPanel extends JPanel {
private final List<MonsterInfo> monsterInfos = new CopyOnWriteArrayList<>();
private final int playerX = 2500; // 玩家固定在小地图中心
private final int playerY = 2500;
private final int minimapSize = 400; // 小地图显示大小
private final int viewRange = 1000; // 小地图显示范围
// 怪物信息类(客户端存储)
static class MonsterInfo {
int id;
int x, y;
int type; // 1=红点, 2=黄点, 3=BOSS
String name;
MonsterInfo(int id, int x, int y, int type, String name) {
this.id = id;
this.x = x;
this.y = y;
this.type = type;
this.name = name;
}
}
public MiniMapPanel() {
setPreferredSize(new Dimension(minimapSize + 50, minimapSize + 50));
setBackground(Color.DARK_GRAY);
}
// 更新怪物位置
public void updateMonsterPosition(int monsterId, int x, int y) {
for (MonsterInfo info : monsterInfos) {
if (info.id == monsterId) {
info.x = x;
info.y = y;
repaint();
return;
}
}
}
// 添加怪物到小地图
public void addMonster(int monsterId, int x, int y, int type, String name) {
monsterInfos.add(new MonsterInfo(monsterId, x, y, type, name));
repaint();
}
// 移除怪物(当怪物死亡时)
public void removeMonster(int monsterId) {
monsterInfos.removeIf(info -> info.id == monsterId);
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// 绘制小地图背景
g2d.setColor(new Color(50, 50, 50));
g2d.fillRect(25, 25, minimapSize, minimapSize);
// 绘制小地图边框
g2d.setColor(Color.WHITE);
g2d.drawRect(25, 25, minimapSize, minimapSize);
// 绘制玩家位置(中心点)
g2d.setColor(Color.GREEN);
int playerMapX = minimapSize / 2 + 25;
int playerMapY = minimapSize / 2 + 25;
g2d.fillOval(playerMapX - 5, playerMapY - 5, 10, 10);
g2d.setColor(Color.WHITE);
g2d.drawString("玩家", playerMapX - 10, playerMapY - 10);
// 绘制怪物红点
for (MonsterInfo monster : monsterInfos) {
if (monster.type == 0) continue; // 不显示的怪物
// 计算怪物在小地图上的相对位置
int monsterMapX = 25 + (int)((monster.x - (playerX - viewRange / 2)) *
minimapSize / viewRange);
int monsterMapY = 25 + (int)((monster.y - (playerY - viewRange / 2)) *
minimapSize / viewRange);
// 只显示在视野范围内的怪物
if (monsterMapX >= 25 && monsterMapX <= 25 + minimapSize &&
monsterMapY >= 25 && monsterMapY <= 25 + minimapSize) {
// 根据类型设置颜色
switch (monster.type) {
case 1: // 红点(普通主动怪)
g2d.setColor(Color.RED);
break;
case 2: // 黄点(被动怪)
g2d.setColor(Color.YELLOW);
break;
case 3: // BOSS(大红色)
g2d.setColor(new Color(255, 0, 0, 200));
g2d.fillOval(monsterMapX - 8, monsterMapY - 8, 16, 16);
g2d.setColor(Color.WHITE);
g2d.drawString("BOSS", monsterMapX - 15, monsterMapY - 15);
continue;
}
// 绘制怪物点
g2d.fillOval(monsterMapX - 4, monsterMapY - 4, 8, 8);
// 绘制怪物名称(简化显示)
if (monster.type == 1) {
g2d.setColor(Color.WHITE);
g2d.drawString(monster.name.substring(0, Math.min(3, monster.name.length())),
monsterMapX - 5, monsterMapY - 10);
}
}
}
// 绘制图例
g2d.setColor(Color.WHITE);
g2d.drawString("小地图图例:", 25, minimapSize + 50);
g2d.setColor(Color.GREEN);
g2d.fillOval(80, minimapSize + 45, 8, 8);
g2d.setColor(Color.WHITE);
g2d.drawString("玩家", 95, minimapSize + 52);
g2d.setColor(Color.RED);
g2d.fillOval(140, minimapSize + 45, 8, 8);
g2d.setColor(Color.WHITE);
g2d.drawString("主动怪", 155, minimapSize + 52);
g2d.setColor(Color.YELLOW);
g2d.fillOval(210, minimapSize + 45, 8, 8);
g2d.setColor(Color.WHITE);
g2d.drawString("被动怪", 225, minimapSize + 52);
}
}
// 6. 客户端类 - 接收服务器封包并更新小地图
class Client {
private final MiniMapPanel minimap;
private final String playerName;
public Client(String playerName, MiniMapPanel minimap) {
this.playerName = playerName;
this.minimap = minimap;
}
// 接收服务器封包并处理
public void receivePacket(Object packet) {
if (packet instanceof S_NPCPack) {
S_NPCPack npcPack = (S_NPCPack) packet;
if (npcPack.getMinimapType() > 0) {
minimap.addMonster(npcPack.getMonsterId(),
npcPack.getX(), npcPack.getY(),
npcPack.getMinimapType(), "怪物" + npcPack.getMonsterId());
System.out.println("客户端收到怪物出现: ID=" + npcPack.getMonsterId());
}
} else if (packet instanceof S_MoveNpcPacket) {
S_MoveNpcPacket movePacket = (S_MoveNpcPacket) packet;
minimap.updateMonsterPosition(movePacket.getMonsterId(),
movePacket.getNewX(), movePacket.getNewY());
System.out.println("客户端收到怪物移动: ID=" + movePacket.getMonsterId() +
" -> (" + movePacket.getNewX() + "," + movePacket.getNewY() + ")");
}
}
}
// 7. 主程序 - 模拟游戏运行
public class MonsterMinimapSystem {
private static GameServer server;
private static MiniMapPanel minimap;
private static JFrame frame;
public static void main(String[] args) {
// 初始化服务器
server = new GameServer();
// 创建小地图UI
minimap = new MiniMapPanel();
// 创建客户端
Client client = new Client("测试玩家", minimap);
server.addClient(client);
// 创建并显示UI
SwingUtilities.invokeLater(() -> {
frame = new JFrame("怪物小地图红点显示系统");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 信息面板
JPanel infoPanel = new JPanel();
infoPanel.setLayout(new FlowLayout());
JLabel infoLabel = new JLabel("模拟所有怪物在小地图显示为红点,红点实时跟随怪物移动");
infoPanel.add(infoLabel);
// 控制面板
JPanel controlPanel = new JPanel();
JButton addMonsterBtn = new JButton("添加新怪物");
JButton startSimulationBtn = new JButton("开始模拟移动");
controlPanel.add(addMonsterBtn);
controlPanel.add(startSimulationBtn);
// 按钮事件
addMonsterBtn.addActionListener(e -> {
// 随机生成怪物
Random rand = new Random();
int x = rand.nextInt(server.getMapWidth());
int y = rand.nextInt(server.getMapHeight());
boolean aggressive = rand.nextBoolean();
boolean boss = rand.nextInt(10) == 0; // 10%几率生成BOSS
int level = rand.nextInt(50) + 1;
String name = (boss ? "BOSS_" : "") +
(aggressive ? "主动怪" : "被动怪") + "_Lv" + level;
Monster monster = new Monster(x, y, name, level, aggressive, boss);
server.addMonster(monster);
System.out.println("添加新怪物: " + name + " 位置=(" + x + "," + y + ")");
});
startSimulationBtn.addActionListener(e -> {
startMonsterMovementSimulation();
});
// 添加到框架
frame.add(infoPanel, BorderLayout.NORTH);
frame.add(minimap, BorderLayout.CENTER);
frame.add(controlPanel, BorderLayout.SOUTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// 初始添加一些怪物
initializeMonsters();
});
}
private static void initializeMonsters() {
// 添加初始怪物
server.addMonster(new Monster(2300, 2400, "狼", 5, true, false));
server.addMonster(new Monster(2600, 2500, "史莱姆", 3, false, false));
server.addMonster(new Monster(2400, 2600, "哥布林", 8, true, false));
server.addMonster(new Monster(2700, 2300, "龙", 45, true, true)); // BOSS
server.addMonster(new Monster(2200, 2700, "鹿", 2, false, false));
System.out.println("初始化完成,添加了5个怪物(包含1个BOSS)");
}
private static void startMonsterMovementSimulation() {
Thread simulationThread = new Thread(() -> {
try {
System.out.println("开始模拟怪物移动...");
for (int i = 0; i < 100; i++) { // 模拟100次移动
server.updateMonsterPositions();
Thread.sleep(1000); // 每秒移动一次
// 更新UI
SwingUtilities.invokeLater(() -> {
minimap.repaint();
});
}
System.out.println("模拟结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
simulationThread.setDaemon(true);
simulationThread.start();
}
}
頁:
[1]