我也不知道有什么实用价值但反正先写着。
思路:
1)强加载区块
2)主动发送区块数据
3)强制渲染区块
强加载
首先是强加载区块,我写了一个简单的类实现:
public class ChunkLoader {
private static final HashMap<String, ArrayList<ChunkPos>> loaders = new HashMap<>();
public static void add(ServerLevel level, ChunkPos center) {
String key = level.dimension().toString();
if (!loaders.containsKey(key)) loaders.put(key, new ArrayList<>());
if (!loaders.get(key).contains(center)) {
loaders.get(key).add(center);
ForgeChunkManager.forceChunk(level, MODID, center.getMiddleBlockPosition(0), center.x, center.z, true, true);
}
}
public static void removeAll(ServerLevel level) {
String key = level.dimension().toString();
if (!loaders.containsKey(key)) return;
if (loaders.get(key).isEmpty()) return;
Iterator<ChunkPos> iterator = loaders.get(key).iterator();
while (iterator.hasNext()) {
ChunkPos center = iterator.next();
iterator.remove();
ForgeChunkManager.forceChunk(level, MODID, center.getMiddleBlockPosition(0), center.x, center.z, false, false);
}
}
}
写得比较草率,主打一个能用就行。
这是在服务端运行的,区块要加载到服务端的区块缓存(ServerChunkCache)中,才能发送对应的区块数据。
如果想安全地强加载区块,请使用Ticket系统,我这里只是分享思路,就简单用forceChunk方法敷衍过去了。
发送区块数据
这部分是mixin得到的,把ChunkMap中的playerLoadedChunk方法拿出来用,最终发送的是一个ClientboundLevelChunkWithLightPacket包。
Mixin
Implements)
public abstract class ChunkMapMixin implements IChunkMap {
@Shadow Nullable protected abstract ChunkHolder getVisibleChunkIfPresent;
Shadow protected abstract void playerLoadedChunk;
public void loadLevelChunk(ServerPlayer player, ChunkPos chunkPos) {
ChunkHolder chunkholder = this.getVisibleChunkIfPresent(chunkPos.toLong());
if (chunkholder == null) return;
LevelChunk levelchunk = chunkholder.getTickingChunk();
if (levelchunk == null) return;
this.playerLoadedChunk(player, new MutableObject<>(), levelchunk);
}
}
默认情况下,区块更新是惰性的,要使用playerLoadedChunk方法,除了单独拎出来用,也可以插入到move方法中:
Inject)
private void justMove(ServerPlayer player, CallbackInfo ci) {
loadLevelChunk(player, ChunkPos.ZERO);
}
这个方法是默认情况下玩家更新区块的方法,插入到这里,等同于为玩家更新额外的区块。
强制渲染
这部分是最复杂的,需要进行非常非常多的mixin。
让我们一步步走。
首先,客户端收到ClientboundLevelChunkWithLightPacket包后需要进行处理,将区块数据存进客户端的区块缓存(ClientChunkCache)中,等待帧渲染将它抓出来渲染。
这里出现了第一次渲染判定,读取区块缓存的时候,要检测区块坐标是否在视距范围内。
那么我们将它干掉。
Mixin
public class ClientChunkCache$StorageMixin {
Inject, cancellable = true)
private void modifyRange(int x, int z, CallbackInfoReturnable<Boolean> cir) {
if (new ChunkPos(*****).equals(new ChunkPos(x, z))) {
cir.setReturnValue(true);
}
}
}
如果轮到检测的这个区块和你想渲染的区块是同一个,就强制通过检测。
说完了缓存,接下来就是渲染。
最核心的渲染是在LevelRenderer里,这个类超级超级长。
我其实不太想介绍这个部分,因为Embeddium在这个类中用Overwrite重写了非常多的方法,比如我下面要说的setupRender:
Mixin
public class LevelRendererMixin {
ModifyVariable, ordinal = 0)
private double modifyX(double x) {
// 改一下x。
return x;
}
// 把y和z也改了,此处省略。
}
这么做可以强制转移渲染的中心点,转移到你想渲染的地方。
是的,这个方法被重写了,重写后以客户端实例的摄像机位置为渲染中心了。
Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
如果要修改渲染中心就去改摄像机位置吧,反正强制渲染特定区块大概率是伴随着摄像机移动的。
如果要考虑不安装Embeddium的情况,就用MixinPlugin区分一下:
public class MixinPlugin implements IMixinConfigPlugin {
Override
public boolean shouldApplyMixin {
if (mixinClassName.equals("com.mafuyu404.examplemod.mixin.LevelRendererMixin")) {
return !isClassLoaded("me.jellysquid.mods.sodium.client.SodiumClientMod"); // Sodium/Embeddium核心类
}
return true;
}
private static boolean isClassLoaded(String className) {
try {
Class.forName(className, false, getClassLoader());
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
// 必要的补全此处省略。
}
不过,真有人会不装Embeddium吗?我测试的时候32视距给我提了快100帧……
在处理完区块渲染,其实还有最后一步,那就是实体渲染。
Mixin
public class ChunkMap$TrackedEntityMixin {
ModifyVariable)
private Vec3 wwa(Vec3 direction) {
// 改一下direction。
return direction;
}
}
很多优化模组都有远处实体剔除的功能,实体渲染其实可以看作与区块渲染独立。
上面这个方法叫updatePlayer,其实是指更新本地玩家区块数据中的实体追踪数据,这里面会计算实体与玩家的距离,从而决定实体是否要渲染。
direction其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。
结语
内容就这么多了,其实是相当鸡肋的东西,之后想到什么再来补充吧。