强制加载并渲染特定区块
我也不知道有什么实用价值但反正先写着。 使用版本:1.20.1 forge 后续有改良版的方案,本篇略显过时。
思路: 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(value = ChunkMap.class)
@Implements(@Interface(iface = IChunkMap .class, prefix = "lazy$"))
public abstract class ChunkMapMixin implements IChunkMap {
@Shadow @Nullable protected abstract ChunkHolder getVisibleChunkIfPresent(long p_140328_);
@Shadow protected abstract void playerLoadedChunk(ServerPlayer p_183761_, MutableObject<ClientboundLevelChunkWithLightPacket> p_183762_, LevelChunk p_183763_);
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(method = "move", at = @At("HEAD"))
private void justMove(ServerPlayer player, CallbackInfo ci) {
loadLevelChunk(player, ChunkPos.ZERO);
}
这个方法是默认情况下玩家更新区块的方法,插入到这里,等同于为玩家更新额外的区块。
强制渲染
这部分是最复杂的,需要进行非常非常多的mixin。 让我们一步步走。 首先,客户端收到ClientboundLevelChunkWithLightPacket包后需要进行处理,将区块数据存进客户端的区块缓存(ClientChunkCache)中,等待帧渲染将它抓出来渲染。 这里出现了第一次渲染判定,读取区块缓存的时候,要检测区块坐标是否在视距范围内。 那么我们将它干掉。
@Mixin(targets = "net.minecraft.client.multiplayer.ClientChunkCache$Storage")
public class ClientChunkCache$StorageMixin {
@Inject(method = "inRange", at = @At("HEAD"), 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(LevelRenderer.class)
public class LevelRendererMixin {
@ModifyVariable(method = "setupRender",at = @At("STORE"), 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(String targetClassName, String mixinClassName) {
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(targets = "net.minecraft.server.level.ChunkMap$TrackedEntity")
public class ChunkMap$TrackedEntityMixin {
@ModifyVariable(method = "updatePlayer", at = @At(value = "STORE"))
private Vec3 wwa(Vec3 direction) {
// 改一下direction。
return direction;
}
}
很多优化模组都有远处实体剔除的功能,实体渲染其实可以看作与区块渲染独立。 上面这个方法叫updatePlayer,其实是指更新本地玩家区块数据中的实体追踪数据,这里面会计算实体与玩家的距离,从而决定实体是否要渲染。 direction其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。
结语
内容就这么多了,其实是相当鸡肋的东西,之后想到什么再来补充吧。