從編程的角度來看,Minecraft 是怎麼樣設計的?
從編程的角度來看,Minecraft 是怎麼樣設計的?如內部的邏輯,一些奇特的機制等等。希望有看過或者研究過 Minecraft 源碼的大牛能分析解答一下。
利益相關:minecraft modder
「怎麼設計的」和「設計成什麼樣的」是兩個不同的問題,「內部的邏輯,一些奇特的機制」都是「設計成什麼樣的問題。當然這兩個問題是緊密相關的。
從軟體工程的角度,minecraft的代碼其實寫的不那麼漂亮,因為它是一個典型的快速開發不斷迭代的項目,看代碼就很容易能看出來,minecraft很多部分都明顯是先有一個方便的原型, 然後部分重構,再部分重構,這麼拖著改到今天這麼大的。所以裡面有很多不規範、臨時的用法在裡面殘留著。今天我就著重說一說MC寫的爛的地方。再順便黑一黑java
舉幾個例子
1、GUI
MC的GUI是lwjgl從頭寫的,它寫的難看到了什麼地步呢,就是隨便一個稍微上一點檔次的mod,都要重新造一遍輪子。MC GuiScreen裡面的一個滑鼠事件是這麼寫的:
protected void mouseClicked(int x, int y, int enable)
{
if (enable == 0)
{
for (int l = 0; l &< this.buttonList.size(); ++l)
{
GuiButton guibutton = (GuiButton)this.buttonList.get(l);
if (guibutton.mousePressed(this.mc, x, y))
{
this.selectedButton = guibutton;
guibutton.playsound(this.mc.getSoundHandler());
this.actionPerformed(guibutton);
}
}
}
}
GuiButton.mousePressed()長這樣:
public boolean mousePressed(Minecraft p_146116_1_, int p_146116_2_, int p_146116_3_)
{
return this.enabled this.visible p_146116_2_ &>= this.xPosition p_146116_3_ &>= this.yPosition p_146116_2_ &< this.xPosition + this.width p_146116_3_ &< this.yPosition + this.height;
}
對,根本沒有事件,也沒有回調,邏輯必須在主窗口類的actionPerformed里處理,這是上個世紀90年代的寫法。所以說稍微有點檔次的mod都要重寫GUI,因為MC本來寫的真是太難看了。
(順便一提,forge用ASM在這裡生插進去了一個事件,雖然這個事件會把別人的按鈕事件也發送給你。所以還算有事件可用,但是這就不算MC本身寫的了。)
2、註冊
我又回頭看了看,註冊這個問題很大,所以我決定不止講那個硬編碼的部分,展開說。
public static void registerBlocks()
{
blockRegistry.addObject(0, "air", (new BlockAir()).setBlockName("air"));
blockRegistry.addObject(1, "stone", (new BlockStone()).setHardness(1.5F).setResistance(10.0F).setStepSound(soundTypePiston).setBlockName("stone").setBlockTextureName("stone"));
blockRegistry.addObject(2, "grass", (new BlockGrass()).setHardness(0.6F).setStepSound(soundTypeGrass).setBlockName("grass").setBlockTextureName("grass"));
blockRegistry.addObject(3, "dirt", (new BlockDirt()).setHardness(0.5F).setStepSound(soundTypeGravel).setBlockName("dirt").setBlockTextureName("dirt"));
Block block = (new Block(Material.rock)).setHardness(2.0F).setResistance(10.0F).setStepSound(soundTypePiston).setBlockName("stonebrick").setCreativeTab(CreativeTabs.tabBlock).setBlockTextureName("cobblestone");
blockRegistry.addObject(4, "cobblestone", block);
Block block1 = (new BlockWood()).setHardness(2.0F).setResistance(5.0F).setStepSound(soundTypeWood).setBlockName("wood").setBlockTextureName("planks");
blockRegistry.addObject(5, "planks", block1);
blockRegistry.addObject(6, "sapling", (new BlockSapling()).setHardness(0.0F).setStepSound(soundTypeGrass).setBlockName("sapling").setBlockTextureName("sapling"));
blockRegistry.addObject(7, "bedrock", (new Block(Material.rock)).setBlockUnbreakable().setResistance(6000000.0F).setStepSound(soundTypePiston).setBlockName("bedrock").disableStats().setCreativeTab(CreativeTabs.tabBlock).setBlockTextureName("bedrock"));
blockRegistry.addObject(8, "flowing_water", (new BlockDynamicLiquid(Material.water)).setHardness(100.0F).setLightOpacity(3).setBlockName("water").disableStats().setBlockTextureName("water_flow"));
blockRegistry.addObject(9, "water", (new BlockStaticLiquid(Material.water)).setHardness(100.0F).setLightOpacity(3).setBlockName("water").disableStats().setBlockTextureName("water_still"));
blockRegistry.addObject(10, "flowing_lava", (new BlockDynamicLiquid(Material.lava)).setHardness(100.0F).setLightLevel(1.0F).setBlockName("lava").disableStats().setBlockTextureName("lava_flow"));
blockRegistry.addObject(11, "lava", (new BlockStaticLiquid(Material.lava)).setHardness(100.0F).setLightLevel(1.0F).setBlockName("lava").disableStats().setBlockTextureName("lava_still"));
blockRegistry.addObject(12, "sand", (new BlockSand()).setHardness(0.5F).setStepSound(soundTypeSand).setBlockName("sand").setBlockTextureName("sand"));
blockRegistry.addObject(13, "gravel", (new BlockGravel()).setHardness(0.6F).setStepSound(soundTypeGravel).setBlockName("gravel").setBlockTextureName("gravel"));
blockRegistry.addObject(14, "gold_ore", (new BlockOre()).setHardness(3.0F).setResistance(5.0F).setStepSound(soundTypePiston).setBlockName("oreGold").setBlockTextureName("gold_ore"));
後面不往下寫了,都是這樣,直到把所有方塊註冊完。這是flash小遊戲的寫法吧。
不過硬編碼和硬編碼也不一樣。你應該注意一下細節,blockRegistry.addObject()的第一個參數是一個int,它是方塊的序號。在RegistryNamespaced里,這個序號作map的key值。序號硬編碼會造成什麼惡果呢?對,序號衝突。自己改序號不好改(所以你看到MC從來沒有刪除過物品或者方塊,也沒有在中間添加過,因為會導致序號改變),別人加物品更頭痛。所以1.6以及之前,mod一直有一個序號衝突的問題。那麼在1.7是否解決了序號問題呢?實際上,沒有。1.7的方塊問題最大的改動是加進了一個UnlocalizedName,意思就是說你以後用這個名字來找方塊,但是實際上代碼內部還是用int編號硬編碼。而1.8最大的改動是讓你在遊戲內命令里不能按照序號give方塊了,而代碼內部還是跟上面沒什麼區別,用int硬編碼。
於是在1.7,fml加進了自動分配序號的功能:
package cpw.mods.fml.common.registry;
private int registerBlock(Block block, String name, int idHint)
{
// handle ItemBlock-before-Block registrations
ItemBlock itemBlock = null;
for (Item item : iItemRegistry.typeSafeIterable()) // find matching ItemBlock
{
if (item instanceof ItemBlock ((ItemBlock) item).field_150939_a == block)
{
itemBlock = (ItemBlock) item;
break;
}
}
if (itemBlock != null) // has ItemBlock, adjust id and clear the slot already occupied by the corresponding item
{
idHint = iItemRegistry.getId(itemBlock);
FMLLog.fine("Found matching ItemBlock %s for Block %s at id %d", itemBlock, block, idHint);
freeSlot(idHint, block); // temporarily free the slot occupied by the Item for the block registration
}
// add
int blockId = iBlockRegistry.add(idHint, name, block, availabilityMap);
if (itemBlock != null) // verify
{
if (blockId != idHint) throw new IllegalStateException(String.format("Block at itemblock id %d insertion failed, got id %d.", idHint, blockId));
verifyItemBlockName(itemBlock);
}
useSlot(blockId); 用的namespace都不是原來的RegistryNamespaced,是fml自己的FMLControlledNamespacedRegistry&,也就是說原來那個已經沒法用了。對,你看fml還加泛型,可以限定這個命名空間給方塊用或者給物品用,這就是人的寫法。 沒錯,這就是藥水類的註冊。也不用什麼register了,直接寫static了。 尼瑪。數組,而且還是定長數組。 附魔的問題好一些,好在什麼地方呢?他的數組長。有時候想想挺可笑的,寫mod遇到這麼大麻煩,就是因為mojang有個數寫的不夠大。 3、性能
((RegistryDelegate.Delegate&
return blockId;
}
看中間那個if,就是在判斷命名空間里有沒有;如果沒有,重新分配序號。
這個就是有fml,幫我們解決了MC的代碼問題。如果fml沒解決呢?public class Potion
{
/** The array of potion types. */
public static final Potion[] potionTypes = new Potion[32];
public static final Potion field_76423_b = null;
public static final Potion moveSpeed = (new Potion(1, false, 8171462)).setPotionName("potion.moveSpeed").setIconIndex(0, 0).func_111184_a(SharedMonsterAttributes.movementSpeed, "91AEAA56-376B-4498-935B-2F7F68070635", 0.20000000298023224D, 2);
public static final Potion moveSlowdown = (new Potion(2, true, 5926017)).setPotionName("potion.moveSlowdown").setIconIndex(1, 0).func_111184_a(SharedMonsterAttributes.movementSpeed, "7107DE5E-7CE8-4030-940E-514C1F160890", -0.15000000596046448D, 2);
public static final Potion digSpeed = (new Potion(3, false, 14270531)).setPotionName("potion.digSpeed").setIconIndex(2, 0).setEffectiveness(1.5D);
public static final Potion digSlowdown = (new Potion(4, true, 4866583)).setPotionName("potion.digSlowDown").setIconIndex(3, 0);
public static final Potion damageBoost = (new PotionAttackDamage(5, false, 9643043)).setPotionName("potion.damageBoost").setIconIndex(4, 0).func_111184_a(SharedMonsterAttributes.attackDamage, "648D7064-6A60-4F59-8ABE-C2C23A6DD7A9", 3.0D, 2);
public static final Potion heal = (new PotionHealth(6, false, 16262179)).setPotionName("potion.heal");
public static final Potion harm = (new PotionHealth(7, true, 4393481)).setPotionName("potion.harm");
public static final Potion jump = (new Potion(8, false, 7889559)).setPotionName("potion.jump").setIconIndex(2, 1);
public static final Potion confusion = (new Potion(9, true, 5578058)).setPotionName("potion.confusion").setIconIndex(3, 1).setEffectiveness(0.25D);
/** The regeneration Potion object. */
public static final Potion regeneration = (new Potion(10, false, 13458603)).setPotionName("potion.regeneration").setIconIndex(7, 0).setEffectiveness(0.25D);
public static final Potion resistance = (new Potion(11, false, 10044730)).setPotionName("potion.resistance").setIconIndex(6, 1);
/** The fire resistance Potion object. */
public static final Potion fireResistance = (new Potion(12, false, 14981690)).setPotionName("potion.fireResistance").setIconIndex(7, 1);
/** The water breathing Potion object. */
public static final Potion waterBreathing = (new Potion(13, false, 3035801)).setPotionName("potion.waterBreathing").setIconIndex(0, 2);
/** The invisibility Potion object. */
public static final Potion invisibility = (new Potion(14, false, 8356754)).setPotionName("potion.invisibility").setIconIndex(0, 1);
/** The blindness Potion object. */
public static final Potion blindness = (new Potion(15, true, 2039587)).setPotionName("potion.blindness").setIconIndex(5, 1).setEffectiveness(0.25D);
/** The night vision Potion object. */
public static final Potion nightVision = (new Potion(16, false, 2039713)).setPotionName("potion.nightVision").setIconIndex(4, 1);
/** The hunger Potion object. */
public static final Potion hunger = (new Potion(17, true, 5797459)).setPotionName("potion.hunger").setIconIndex(1, 1);
/** The weakness Potion object. */
public static final Potion weakness = (new PotionAttackDamage(18, true, 4738376)).setPotionName("potion.weakness").setIconIndex(5, 0).func_111184_a(SharedMonsterAttributes.attackDamage, "22653B89-116E-49DC-9B6B-9971489B5BE5", 2.0D, 0);
/** The poison Potion object. */
public static final Potion poison = (new Potion(19, true, 5149489)).setPotionName("potion.poison").setIconIndex(6, 0).setEffectiveness(0.25D);
/** The wither Potion object. */
public static final Potion wither = (new Potion(20, true, 3484199)).setPotionName("potion.wither").setIconIndex(1, 2).setEffectiveness(0.25D);
public static final Potion field_76434_w = (new PotionHealthBoost(21, false, 16284963)).setPotionName("potion.healthBoost").setIconIndex(2, 2).func_111184_a(SharedMonsterAttributes.maxHealth, "5D6F0BA2-1186-46AC-B896-C61C5CEE99CC", 4.0D, 0);
public static final Potion field_76444_x = (new PotionAbsoption(22, false, 2445989)).setPotionName("potion.absorption").setIconIndex(2, 2);
public static final Potion field_76443_y = (new PotionHealth(23, false, 16262179)).setPotionName("potion.saturation");
public static final Potion field_76442_z = null;
public static final Potion field_76409_A = null;
public static final Potion field_76410_B = null;
public static final Potion field_76411_C = null;
public static final Potion field_76405_D = null;
public static final Potion field_76406_E = null;
public static final Potion field_76407_F = null;
public static final Potion field_76408_G = null;
好吧,你是怎麼寫的我也不想管了,我就想加幾種自己定義的藥水效果。然後我們就看到了一行代碼:public static final Potion[] potionTypes = new Potion[32];
怎麼辦?老實說,沒什麼好辦法,因為fml沒幫你解決這個問題。固然你可以換掉這個數組給它擴容,但是這必然會導致mod和mod不兼容。反正,現在版本依然有大量的藥水效果衝突的問題存在。比如你可以在1.7.10裝個IC再裝個環境,看饑渴效果是怎麼變成輻射效果把你秒了的。TC的藥水數量已經自己都在那幾個空里裝不下了,他自己實現了一個potion數組,但是他不是fml,我們也沒法都轉移過去,所以這個問題也沒什麼好辦法。public abstract class Enchantment
{
public static final Enchantment[] enchantmentsList = new Enchantment[256];
/** The list of enchantments applicable by the anvil from a book */
public static final Enchantment[] enchantmentsBookList;
/** Converts environmental damage to armour damage */
public static final Enchantment protection = new EnchantmentProtection(0, 10, 0);
/** Protection against fire */
public static final Enchantment fireProtection = new EnchantmentProtection(1, 5, 1);
/** Less fall damage */
public static final Enchantment featherFalling = new EnchantmentProtection(2, 5, 2);
/** Protection against explosions */
public static final Enchantment blastProtection = new EnchantmentProtection(3, 2, 3);
/** Protection against projectile entities (e.g. arrows) */
public static final Enchantment projectileProtection = new EnchantmentProtection(4, 5, 4);
/** Decreases the rate of air loss underwater; increases time between damage while suffocating */
public static final Enchantment respiration = new EnchantmentOxygen(5, 2);
/** Increases underwater mining rate */
public static final Enchantment aquaAffinity = new EnchantmentWaterWorker(6, 2);
public static final Enchantment thorns = new EnchantmentThorns(7, 1);
/** Extra damage to mobs */
public static final Enchantment sharpness = new EnchantmentDamage(16, 10, 0);
/** Extra damage to zombies, zombie pigmen and skeletons */
public static final Enchantment smite = new EnchantmentDamage(17, 5, 1);
/** Extra damage to spiders, cave spiders and silverfish */
public static final Enchantment baneOfArthropods = new EnchantmentDamage(18, 5, 2);
/** Knocks mob and players backwards upon hit */
public static final Enchantment knockback = new EnchantmentKnockback(19, 5);
/** Lights mobs on fire */
public static final Enchantment fireAspect = new EnchantmentFireAspect(20, 2);
/** Mobs have a chance to drop more loot */
public static final Enchantment looting = new EnchantmentLootBonus(21, 2, EnumEnchantmentType.weapon);
/** Faster resource gathering while in use */
public static final Enchantment efficiency = new EnchantmentDigging(32, 10);
/**
* Blocks mined will drop themselves, even if it should drop something else (e.g. stone will drop stone, not
* cobblestone)
*/
public static final Enchantment silkTouch = new EnchantmentUntouching(33, 1);
/** Sometimes, the tool"s durability will not be spent when the tool is used */
public static final Enchantment unbreaking = new EnchantmentDurability(34, 5);
/** Can multiply the drop rate of items from blocks */
public static final Enchantment fortune = new EnchantmentLootBonus(35, 2, EnumEnchantmentType.digger);
/** Power enchantment for bows, add"s extra damage to arrows. */
public static final Enchantment power = new EnchantmentArrowDamage(48, 10);
/** Knockback enchantments for bows, the arrows will knockback the target when hit. */
public static final Enchantment punch = new EnchantmentArrowKnockback(49, 2);
/** Flame enchantment for bows. Arrows fired by the bow will be on fire. Any target hit will also set on fire. */
public static final Enchantment flame = new EnchantmentArrowFire(50, 2);
/**
* Infinity enchantment for bows. The bow will not consume arrows anymore, but will still required at least one
* arrow on inventory use the bow.
*/
public static final Enchantment infinity = new EnchantmentArrowInfinite(51, 1);
public static final Enchantment field_151370_z = new EnchantmentLootBonus(61, 2, EnumEnchantmentType.fishing_rod);
public static final Enchantment field_151369_A = new EnchantmentFishingSpeed(62, 2, EnumEnchantmentType.fishing_rod);
性能這一塊我要分兩部分,先黑java,再黑mojang。
沒錯,java性能就是差,你來打我啊。
打完了沒有,我要回家了。
好吧,讓我們認真來說,java性能就是差。當然mc也跟所有的java程序一樣,裡面充滿了new new new new,耗內存比耗CPU還多,這麼個小遊戲居然沒個2G內存帶不起來。畫圖性能也不咋地,當然最新的lwjgl3.0有了一定的好轉,不過稍早的mc用的還是2.4,簡直慘,就這麼個根本沒有幾個多邊形的遊戲,看著不同的地方能差十幾幀。
演算法倒沒什麼可黑的,mc主要吃CPU的演算法都寫的中規中矩網上抄的,按照java規範來,快不到哪去但也沒法寫的再快了。比如PathNavigate和WorldGen系列,演算法比較複雜,詳細的就不貼了。
好,黑java差不多,該黑mojang了。先給你們看看1.8特性列表裡的一欄。
渲染 圖像優化
- 顯著的提高 FPS 和性能
- 每個世界(主世界、下界、末路之地) 現在各自獨立運作
- 區塊渲染和區塊重建現在使用多線程- 超快速的區塊渲染!
- 重寫了區塊序列
- Better visibility culling code
- 生物尋路現在是多線程的
- 重寫儲物系統
- 礦物生成現在較以前快了2倍
- 現在只有透明方塊才可以被渲染為透明的(移除X光材質包的使用)
- Dropped items now face the player in all three directions on fast graphics
- 重寫方塊是如何被渲染的
- 重寫方塊數據是如何被處理的
- 改善遊戲並使"cooler things" 得以被實現
也就是說,直到1.8之前,區塊重建、礦物生成、多世界、生物尋路都是單線程的。你要知道,這些可都是性能大頭,這些部分是單線程的,意味著mc伺服器基本上就是單線程的……
坑爹啊!
還是給你們看一看mc的線程狀況吧。以下以1.7.10版本為準。
怎麼知道mc哪裡有線程呢?搜索Thread和synchronized關鍵字。
好嘛,有OpenGL用的,有OpenAL用的,有twitch流用的,有Crash的時候列印錯誤信息用的,有網關IO用的,就是沒有演算法用的……
再看看synchronized
跟上面差不多,有文件IO的,有網路IO的,有圖像IO的,有視頻IO的……
不過world類里那兩個是真的,Chunk和Gen確實有多線程,這個等黑完了我再誇一誇。
好吧,有沒有可能我們漏掉了什麼地方,實際上mc多數代碼早已悄悄躲進幾個大線程里,普通代碼根本不用加鎖呢?我們來調試一下看看:
什麼亂七八糟線程都有,就是看不見演算法分線程。實際上MC代碼就全都運行在就那一個高亮的Server Thread裡面。
1.8進步了,不過有限。如果照樣搜一遍的話,我們會看到ChunkRender里有了Thread和synchronized,對應上面的方塊渲染。
珍愛伺服器,遠離1.7。希望微軟趕緊用C++把mc重寫。然後把學不會C++的都趕走,這樣我就是領軍人物了。如果能招我進微軟就更好了。
4、API
現在想了想,API的問題應該放到第一去講的。但是實際上上面三個都是我們modder的眼中釘肉中刺,相比之下API的問題都已經習慣了,所以我現在才想起來這個問題。
那麼我們就講講mc里的API怎麼寫的。
問:MC里的API怎麼寫的?
答:MC根本沒有API。
我們剛剛提過,MC是從一個小遊戲慢慢變大的,所以裡面有很多歷史遺留寫法。細節上的歷史遺留講的很多了,那麼架構上最大的遺留問題是什麼呢?那就是API。
MC的架構,根本沒有給寫API留下什麼空間。
仍以一個方塊為例,MC里採用一種類似享元模式的設計模式處理方塊,每一種方塊都是一個對象。為了創造具有特殊行為的方塊,需要寫一個新類繼承Block類,並且Override相應的函數以實現自己的行為。那麼,Block類大概有多大呢?
2631行,大概200個可以被重載的函數,其中大部分都在某個方塊類里被重載了。
舉個特定方塊類的例子,比如礦物方塊。
public class BlockOre extends Block
{
public BlockOre()
{
super(Material.rock);
this.setCreativeTab(CreativeTabs.tabBlock);
}
public Item getItemDropped(int p_149650_1_, Random p_149650_2_, int p_149650_3_)
{
return this == Blocks.coal_ore ? Items.coal : (this == Blocks.diamond_ore ? Items.diamond : (this == Blocks.lapis_ore ? Items.dye : (this == Blocks.emerald_ore ? Items.emerald : (this == Blocks.quartz_ore ? Items.quartz : Item.getItemFromBlock(this)))));
}
public int quantityDropped(Random p_149745_1_)
{
return this == Blocks.lapis_ore ? 4 + p_149745_1_.nextInt(5) : 1;
}
public int quantityDroppedWithBonus(int p_149679_1_, Random p_149679_2_)
{
if (p_149679_1_ &> 0 Item.getItemFromBlock(this) != this.getItemDropped(0, p_149679_2_, p_149679_1_))
{
int j = p_149679_2_.nextInt(p_149679_1_ + 2) - 1;
if (j &< 0) { j = 0; } return this.quantityDropped(p_149679_2_) * (j + 1); } else { return this.quantityDropped(p_149679_2_); } } public void dropBlockAsItemWithChance(World p_149690_1_, int p_149690_2_, int p_149690_3_, int p_149690_4_, int p_149690_5_, float p_149690_6_, int p_149690_7_) { super.dropBlockAsItemWithChance(p_149690_1_, p_149690_2_, p_149690_3_, p_149690_4_, p_149690_5_, p_149690_6_, p_149690_7_); } private Random rand = new Random(); @Override public int getExpDrop(IBlockAccess p_149690_1_, int p_149690_5_, int p_149690_7_) { if (this.getItemDropped(p_149690_5_, rand, p_149690_7_) != Item.getItemFromBlock(this)) { int j1 = 0; if (this == Blocks.coal_ore) { j1 = MathHelper.getRandomIntegerInRange(rand, 0, 2); } else if (this == Blocks.diamond_ore) { j1 = MathHelper.getRandomIntegerInRange(rand, 3, 7); } else if (this == Blocks.emerald_ore) { j1 = MathHelper.getRandomIntegerInRange(rand, 3, 7); } else if (this == Blocks.lapis_ore) { j1 = MathHelper.getRandomIntegerInRange(rand, 2, 5); } else if (this == Blocks.quartz_ore) { j1 = MathHelper.getRandomIntegerInRange(rand, 2, 5); } return j1; } return 0; } public int damageDropped(int p_149692_1_) { return this == Blocks.lapis_ore ? 4 : 0; } }
看,新方塊類就這麼寫,繼承一下Block,然後就開始胡改了。不管是被按照Block調用還是調用別的函數,都沒有中間層抽象,都是直接調用。耦合成這個樣子,怎麼寫API?無怪mojang 1.6的時候就說要寫API了,都這麼久了還一點消息沒有,這怎麼能寫出來API。
現在所有的modder寫mod,都必須拿著反編譯反混淆過的源碼,大量地直接使用MC里原有的基類和調用函數,把自己的代碼完全嵌入MC的架構里,否則沒法寫。
反編譯是否違法呢?當然違法,人家可是商業軟體。但是有什麼辦法,我還想有成熟的API可用呢,mojang自己不爭氣寫不出來API,又不敢關門攆人,所以就變成了現在這個樣子,mojang默許整個MC社區使用MC的反編譯代碼(這個工程叫MCP)來寫mod。所以說mod社區對微軟收購這件事這麼敏感,因為本來就名不正言不順。
如果MC有了API,大概會是什麼樣子呢?雖然目前還沒有可以整個用來寫mod的API,不過一個子集已經有了,叫bukkit。bukkit很能說明一個有了API的開發,是一種什麼感受。bukkit大概總共就這麼長:
org.bukkit
More generalized classes in the API.
org.bukkit.block
Classes used to manipulate the voxels in a world, including special states.
org.bukkit.block.banner
org.bukkit.command
Classes relating to handling specialized non-chat player input.
org.bukkit.command.defaults
Commands for emulating the Minecraft commands and other necessary ones for use by a Bukkit implementation.
org.bukkit.configuration
Classes dedicated to handling a plugin"s runtime configuration.
org.bukkit.configuration.file
Classes dedicated facilitating configurations to be read and stored on the filesystem.
org.bukkit.configuration.serialization
Classes dedicated to being able to perform serialization specialized for the Bukkit configuration implementation.
org.bukkit.conversations
Classes dedicated to facilitate direct player-to-plugin communication.
org.bukkit.enchantments
Classes relating to the specialized enhancements to item stacks, as part of the meta data.
org.bukkit.entity
Interfaces for non-voxel objects that can exist in a world, including all players, monsters, projectiles, etc.
org.bukkit.entity.minecart
Interfaces for various Minecart types.
org.bukkit.event
Classes dedicated to handling triggered code executions.
org.bukkit.event.block
Events relating to when a block is changed or interacts with the world.
org.bukkit.event.enchantment
Events triggered from an enchantment table.
org.bukkit.event.entity
Events relating to entities, excluding some directly referencing some more specific entity types.
org.bukkit.event.hanging
Events relating to entities that hang.
org.bukkit.event.inventory
Events relating to inventory manipulation.
org.bukkit.event.painting
Events relating to paintings, but deprecated for more general hanging events.
org.bukkit.event.player
Events relating to players.
org.bukkit.event.server
Events relating to programmatic state changes on the server.
org.bukkit.event.vehicle
Events relating to vehicular entities.
org.bukkit.event.weather
Events relating to weather.
org.bukkit.event.world
Events triggered by various world states or changes.
org.bukkit.generator
Classes to facilitate world generation implementation.
org.bukkit.help
Classes used to manipulate the default command and topic assistance system.
org.bukkit.inventory
Classes involved in manipulating player inventories and item interactions.
org.bukkit.inventory.meta
The interfaces used when manipulating extra data can can be stored inside item stacks.
org.bukkit.map
Classes to facilitate plugin handling of map displays.
org.bukkit.material
Classes that represents various voxel types and states.
org.bukkit.metadata
Classes dedicated to providing a layer of plugin specified data on various Minecraft concepts.
org.bukkit.permissions
Classes dedicated to providing binary state properties to players.
org.bukkit.plugin
Classes specifically relating to loading software modules at runtime.
org.bukkit.plugin.java
Classes for handling plugins written in java.
org.bukkit.plugin.messaging
Classes dedicated to specialized plugin to client protocols.
org.bukkit.potion
Classes to represent various potion properties and manipulation.
org.bukkit.projectiles
Classes to represent the source of a projectile
org.bukkit.scheduler
Classes dedicated to letting plugins run code at specific time intervals, including thread safety.
org.bukkit.scoreboard
Interfaces used to manage the client side score display system.
org.bukkit.util
Multi and single purpose classes to facilitate various programmatic concepts.
org.bukkit.util.io
Classes used to facilitate stream processing for specific Bukkit concepts.
org.bukkit.util.noise
Classes dedicated to facilitating deterministic noise.
org.bukkit.util.permissions
Static methods for miscellaneous permission functionality.
相比之下MC包的數量大概是他的三倍,類的數量……我不想算了,因為每個物品方塊都是類。
你要知道,這些部分全都是解耦的。bukkit這些介面是真正的介面,bukkit並不會規定其具體實現,每一版craftbukkit都會有所不同。而且bukkit向下兼容,其內容簡潔易懂,符合軟體工程原則,使用舒適度遠超讀MC那些詰屈聱牙的大段代碼。這就是一個真正的API該有的樣子。
forge是不是API呢?實際上,不是。在寫mod的時候,只有兩個部分要跟forge交互:
1.forge.event。原版MC是沒有事件系統的,這個樣子連調用自己的代碼都辦不到。forge加進了一些事件,包括MC初始化事件,讓你有機會處理事務。
2.register。這基本上是一些便利類,能簡化你註冊方塊、物品、翻譯文件一類的流程,可以寫一個函數搞定。
這些不是API,實際上它們沒有抽象任何事情,你真正需要解決的問題還是得到源碼里搞定,他只是幫你解決不該由你解決的問題而已。
相信眼尖的同學在線程里已經看見了,MC的網路連接用的是netty。netty設計的大致還是沒什麼問題的,有問題這裡也不提了。我們就看看netty IO的上下游,MC發包和解包怎麼寫的。
先看一下類,有Packet類,每種Packet有個類,有INetHandler,有NetHandler,寫的挺好嘛,一個一個看。
public abstract class Packet
{
public static Packet generatePacket(BiMap p_148839_0_, int p_148839_1_)
{
try
{
Class oclass = (Class)p_148839_0_.get(Integer.valueOf(p_148839_1_));
return oclass == null ? null : (Packet)oclass.newInstance();
}
catch (Exception exception)
{
logger.error("Couldn"t create packet " + p_148839_1_, exception);
return null;
}
}
public abstract void readPacketData(PacketBuffer p_148837_1_) throws IOException;
public abstract void writePacketData(PacketBuffer p_148840_1_) throws IOException;
public abstract void processPacket(INetHandler p_148833_1_);
}
不錯,有抽象基類,還自帶一個小工廠。找個類看看。
public class C09PacketHeldItemChange extends Packet
{
public C09PacketHeldItemChange(int slot)
{
this.hold = slot;
}
public void readPacketData(PacketBuffer buffer) throws IOException
{
this.bit= buffer.readShort();
}
public void writePacketData(PacketBuffer buffer) throws IOException
{
buffer.writeShort(this.bit);
}
public void processPacket(INetHandlerPlayServer nethandler)
{
nethandler.processHeldItemChange(this);
}
public void processPacket(INetHandler nethandler)
{
this.processPacket((INetHandlerPlayServer)nethandler);
}
}
……等等,解包之後的實現哪去了?
public class NetworkManager extends SimpleChannelInboundHandler
public void processReceivedPackets()
{
if (this.netHandler != null)
{
for (int i = 1000; !this.receivedPacketsQueue.isEmpty() i &>= 0; --i)
{
Packet packet = (Packet)this.receivedPacketsQueue.poll();
packet.processPacket(this.netHandler);
}
this.netHandler.onNetworkTick();
}
this.channel.flush();
}
這……
public class NetHandlerPlayServer implements INetHandlerPlayServer
{
public void processAnimation(C0APacketAnimation p_147350_1_)
{
this.playerEntity.func_143004_u();
if (p_147350_1_.func_149421_d() == 1)
{
this.playerEntity.swingItem();
}
}
public void processEntityAction(C0BPacketEntityAction p_147357_1_)
{
this.playerEntity.func_143004_u();
if (p_147357_1_.func_149513_d() == 1)
{
this.playerEntity.setSneaking(true);
}
else if (p_147357_1_.func_149513_d() == 2)
{
this.playerEntity.setSneaking(false);
}
else if (p_147357_1_.func_149513_d() == 4)
{
this.playerEntity.setSprinting(true);
}
else if (p_147357_1_.func_149513_d() == 5)
{
this.playerEntity.setSprinting(false);
}
else if (p_147357_1_.func_149513_d() == 3)
{
this.playerEntity.wakeUpPlayer(false, true, true);
this.hasMoved = false;
}
else if (p_147357_1_.func_149513_d() == 6)
{
if (this.playerEntity.ridingEntity != null this.playerEntity.ridingEntity instanceof EntityHorse)
{
((EntityHorse)this.playerEntity.ridingEntity).setJumpPower(p_147357_1_.func_149512_e());
}
}
else if (p_147357_1_.func_149513_d() == 7 this.playerEntity.ridingEntity != null this.playerEntity.ridingEntity instanceof EntityHorse)
{
((EntityHorse)this.playerEntity.ridingEntity).openGUI(this.playerEntity);
}
}
public void processHeldItemChange(C09PacketHeldItemChange p_147355_1_)
{
if (p_147355_1_.func_149614_c() &>= 0 p_147355_1_.func_149614_c() &< InventoryPlayer.getHotbarSize())
{
this.playerEntity.inventory.currentItem = p_147355_1_.func_149614_c();
this.playerEntity.func_143004_u();
}
else
{
logger.warn(this.playerEntity.getCommandSenderName() + " tried to set an invalid carried item");
}
}
(...)
}
……尼瑪。
這解包的具體實現是寫在Handler類里的,把每個被Handle的類的實現都寫在Handler里
,比硬編碼還硬編碼。註冊那些硬編碼至少還是數據,只是難看點,不妨礙你繼續往數組裡加數據;這硬編碼的是函數,根本不可能加。
於是我們很奇怪,介面呢?我們不是還有一個介面,可以用來實現具體解包過程嗎?
public interface INetHandlerPlayServer extends INetHandler
{
void processAnimation(C0APacketAnimation p_147350_1_);
void processChatMessage(C01PacketChatMessage p_147354_1_);
void processPlayerDigging(C07PacketPlayerDigging p_147345_1_);
void processHeldItemChange(C09PacketHeldItemChange p_147355_1_);
(...)
}
讓我笑一會,哈哈哈哈哈哈哈。
正常人寫介面會有很多子類,然後在介面里定義一組對應同一個抽象概念的函數。這個介面只有一個子類,然後在介面里定義一堆互相平行的具體實現。這個介面存在的意義是什麼。
MC的網路的抽象層爛到這個地步,已經完全沒法復用了。幸好,天無絕人之路,netty的抽象層夠多,可以給我們其他的解決方案。
這是netty的管道圖,可以非常直觀地看出,在一個Channel內部,還可以定義許多個Handler進行處理。MC只用了他自己的幾個Handler,我們可以再定義自己的Handler處理packet。實際上,大型mod多半這麼處理的。FML也給了相應的註冊類和API,首先提供了FMLEventChannel來直達netty層,適合熟練使用netty的用戶;對於需求簡單、不會netty的用戶(比如我)還封裝了一個SimpleNetworkWrapper,提供了簡單的直接IO。和MC一比,FML寫的真是好,各個抽象層都考慮到,而且都對齊。誰叫MC已經爛到一定程度了呢。想吐的槽都吐差不多了,再寫點總結補充就結束了吧。
11.30更新:見後面地形生成和總結部分。過百贊了知乎小透明好開森,謝謝大家!
12.3更新:謝大家厚愛。回答一位 @兔子先生 的問題,見文章:答兔子先生,歡迎討論。
研究過MC地形生成和基礎架構的強答一發。
長文預警。
先說架構。
MC分客戶端和服務端兩面,客戶端主要負責渲染,服務端是本地伺服器或者遠程伺服器(本地遊戲兩個都要跑,真正的MC服只跑服務端)。其實這倆就共享一個IWorld介面,然後這個介面裡面亂的一塌糊塗。
地圖的基礎單元是chunk,就是16*256*16的大方塊,裡面包含小方塊。小方塊可以是普通IBlock,可以帶特殊數據(metadata),也可以有TileEntity(按照方塊處理的實體)。區塊裡面還存了Entity就是實體(人啊,殭屍啊,豬啊還有掉落的物品之類的),實體的位置和方向都是浮點數float或double(雙精度),因此擁有更大的自由度。
前後端(客戶端和服務端)存儲數據都靠的這個chunk,傳輸和生成也是以chunk為單位,所以會看到伺服器卡的時候是一塊一塊冒出來的。chunk放到HashMap裡面備用。
保存(1.2之後的原版服務端anvil文件格式)把32*32個chunk放到一個.mca(老版是.mcp)文件裡面,用一種碎片化管理的方式來節省空間同時減少IO句柄數量,具體的格式MC Wiki上有詳細的解釋。
所有MC的存儲數據(注意不包括方塊和音樂本身這種理論上是做死了的數據)都用一種二進位數據格式存儲(NBT Named Binary Tags 命名二進位標籤 ——Minecraft Wiki),裡面包含數組數據、浮點數、整數或者字元串,其實結構相當簡單,項目有樹形結構,每個子項還有字元串形式的名字。它其實可以和JSON簡單互譯,做命令方塊地圖時會用到的JSON格式的實體數據就是這個東西。
服務端動態載入/保存區塊,然後用一個單線程來每個tick更新整個地圖(所以很慢啊喂!),具體方式既粗暴又簡單:遍歷每個方塊,看到有計劃更新(比如水、岩漿、刷怪籠、植物這種)的就讓他更新一下,看到TileEntity或者Entity每個tick都更新,同時處理玩家的數據:放方塊/砸方塊/開箱子/熔鐵錠等等。對了,服務端還要負責更新光照(看後面)。
要是玩家走到了沒有生成過的荒野,就會啟動區塊生成器來生成新的區塊。生成這個特別複雜,也是我主要研究的問題,一會兒再講。
客戶端維護一個非常 *智障* 的GUI系統,裡面的渲染基本上都是legacy opengl直接瞎寫的,GUI更新基本上都是全屏更新的,滑鼠行動基本上全是輪詢的,根本不管什麼事件啦dirty區域啦緩存啦,就兩個字:粗暴。當然MC總共也沒什麼GUI好談,所以也就算了。
這裡提一句其實伺服器裡面花花綠綠的字元的實現原理也非常暴力,就是用小結號§來代表下一個字元是格式轉義字元,從k到r(好像?)都代表不同的意義,顏色或者加粗或者傾斜或者隨機顏色,封面MINECRAFT大字旁邊那行字就是用這個渲染的。什麼?你問§怎麼辦?沒辦法。(攤手)
除了GUI就是遊戲主要的渲染部分。MC應用15年前的opengl技術成功地把每一個chunk編譯成16個list(opengl中的列表,可以理解為把要畫的步驟先告訴顯卡以後再用只要告訴他列表的號碼就行了,用來提升效率),每個16*16*16,不包括TileEntity和Entity——它們是每幀單獨渲染的,所以MC生物一多就卡,而用bug透視的時候看不到方塊但是能看得到生物和箱子。
客戶端每幀要是看到新的改變過的chunk(被什麼東西更新過了)就把它和它四周的chunk都重新生成一遍list(這裡你會看到要是伺服器載入很慢的話最遠處的chunk是看得到截面的),這個過程叫做tessellation,就是把每個小的面片(長方形)放到一起組成一個大的頂點數組(就是一個大模型),一股腦兒傳給顯卡(因為從cpu傳連續數據比零零散散地傳數據要快很多)。
最後渲染的時候也是通過數學計算把在視野(按照玩家視角)中的list找出來一一調用渲染。這裡有個小bug,就是篩選是否在視野中是假定這個list裡面的所有東西都包含在這16*16*16的大方塊裡面的,所以如果你搞一個信標的話,它的光柱會一直延伸到天空上,但是一旦整個list被判斷不可見,這個光柱也會一起消失掉。
至於光照,光照也是在伺服器端算好的(是不是難以想像?),我們看到的有漸變的平滑光照也是在方塊光照上加工計算出來的。
光照有兩部分,一部分叫做天光(sky light),也就是不算任何方塊光源這個位置的光照強度。細節上,他是這麼計算的:如果他直接被陽光照射(上面沒有別的方塊),那麼亮度是16(或者說15,取決於你是從0開始數數還是從1開始),如果上面一個是半透明方塊,那麼亮度是上面的亮度減去版透明方塊的阻礙值(比如樹葉是2,玻璃是1),如果上面是不透明方塊(比如石頭),那麼亮度是0。你可能要問,山洞裡面也不是全黑啊?別急,慢慢講。
區塊生成的時候只會這樣一遍地生成最簡單的光照,而把光照的擴散放到一個隊列裡面等待服務端更新時一點點處理,每個tick能上限好像是1個事件。(理論上這是為了加快速度不讓光照計算拖慢伺服器來著,然而。。)處理事件是一個遞歸的過程,從這個方塊出發,如果他6個方向(上下左右前後)的方塊的亮度大於等於它,那就不遞歸(這種時候定義空氣的光阻為1),否則把那個方向上的方塊+(自身亮度-1)放進隊列。當然要是減到0就不動了。
按照mojang員工的天真想法,既然光照最大是15(因為我從0開始數數),每一格至少減弱1,那麼跟我源頭上這一個坐標差距大於十五的我就不用管了咯?所以他們限制每次遞歸最多遞歸到這麼遠,之後不管亮度多少都留到下一個tick去吧。但是這裡會有一個bug:如果山洞頂上被開了一個洞,從天上漏光,結果從山洞邊緣開始計算天光,算到洞附近發現開始變亮了,但是又礙於距離限制不能再算下去,所以有時候就會看到非常突兀的亮度變化。
光照的另一部分叫做方塊光(block light),也就是螢石啊南瓜燈啊火柴啊這種方塊的燈光。這種也是碰到空氣就-1,碰到不透明的石頭就變成0,碰到半透明的就減去他的光阻量(代碼裡面叫做opacity,意為不透明度)。然後每個方塊上的亮度以最大值為準,同樣塞到前面的那個隊列裡面。服務端更新的時候計算。
光照當然是用來渲染的,而這也是MC渲染極其重要的一部分。
每個方塊的光照強度等於方塊光和天光的最大值,那麼晚上怎麼辦呢?其實天光在晚上的亮度就等於 原先亮度 - 夜晚的亮度降,也就是原來12級光,白天是15級陽光,算出來就是12級,晚上變成11級月光,就是 12 - (15 - 11) = 8級光。當然這個值不能小於0。
最早的光照對應就是一個線性的亮度,15級就是100%的亮度,7級就是50%,0級就是0%,後來發現這樣效果不好,就改成了對數型:第(n-1)級的亮度是第n級的90%,這樣亮度變化比較自然,而且哪怕光照為0也不是全黑(地獄/下界的這個係數是90%,0級光實際上還有46%的亮度,所以顯得下界特別亮)。
後來覺得還不好,所以就乾脆弄了一個二維的亮度圖,橫坐標是天光,縱坐標是方塊光,這個在minecraft.jar裡面找得到一個紋理文件(哎呀名字我忘了),就是這個。因此可以看到方塊光偏暖色,天光偏冷色,也是這麼來的。
渲染更重要的當然就是大家關心的紋理,所謂MC像素之稱的來源之一。
如果打開beta以後的minecraft,jar文件就會看到,紋理圖片都是分開的圖片,但是一個list(還記得list嗎)只能用一個紋理單元,因此MC就用了一種取巧的做法:程序啟動的/更換材質包時候把所有要用的紋理載入一遍然後拼到一張圖上面,有時候會存到.minecraft/下的一個臨時文件夾裡面,再載入進顯卡,就可以一起用了。當然Entity和TileEntity的紋理是不用這麼乾的,因為他們每個都是獨立渲染的,不需要這麼弄。
這也是為什麼MC的API要求方塊必須在使用之前就在Registry裡面註冊好,這樣才能知道你到底要哪些紋理啊。(貌似這個問題在1.8採用了新的model模式後有了改善)這也是為什麼在切換語言之後MC都要卡好久,因為切換語言也算作切換材質包,要把所有的小紋理圖片再拼接一遍,還要把所有的chunk已經生成好的list重新生成一遍。(卡!)
然後就要根據玩家的位置,角度,以及人稱(第一人稱/背後第三人稱/面對第二人稱)來計算投影矩陣(也就是擺攝像機)。這個過程是在客戶端處理的,所以就算服務端卡爆了你的腦袋還是可以自由旋轉的,至於為什麼你可以看到多人中別的玩家的腦袋轉,我猜是時不時地傳一些數據回去做插值弄的。
最後直接丟給opengl畫出來,三維渲染部分就基本完成了。其實這裡MC還允許像光影mod那樣的後期處理(post-processing),1.7左右的版本有一個「Super Secret Setting」的按鈕在遊戲暫停的設置裡面,點一下可以在好多後期特效之間切換。
還要畫天空、太陽、月亮、天氣和雲層,當然太陽月亮就是貼個圖(月有陰晴圓缺哦,紋理文件有一個專門管月相的),下雨下雪就是一個動畫,雲層也就是一堆半透明的方塊。
最後一個部分:第一人稱的手和手上拿的東西,這個也是三維渲染的,不過難度不大,相信大家想想就明白了,也就不多說了。
然後是介於三維和二維之間的粒子系統。粒子系統其實原來是用來模擬像水滴、碎片、樹葉這樣數量巨大的東西,以至於渲染成真正的三維模型代價過高,不得不用一個永遠對著玩家的面來代替,後來就管所有這種永遠面向玩家的面叫做「粒子」(particle),或者「標誌板」(bulletin board)。
MC的粒子系統主要都是一些效果:入水/釣魚是的水滴,吃飯時的食物殘渣,多人模式中別人頭頂上的名字,魔葯產生的效果,經驗球,爆炸的「衝擊波」,都是粒子。(是不是有點大)
反正粒子一般放在同樣的三維環境中渲染,用同樣的投影矩陣,但是往往會被歸為一個特殊的粒子引擎,因為它們作為沒有體積的物體卻經常要表現出一種重力感(就比如說水滴會跳起來再落下去),不能放到一般的三維場景中渲染。
三維畫完了是二維:下面的道具欄和聊天內容這種,按E可以賽艇(劃掉)打開的完整物品欄(inventories)、各種可交互菜單(箱子、鐵氈、熔爐、村民……)。這些凌駕於所有三維內容之上(因為不會被擋掉),最後被渲染,同時也充當與玩家交互的功能。(所以說MC的GUI很亂啦。。這裡都不分清楚的)
說到交互,肯定大家會想,電腦到底是怎麼知道我選中了哪個面,往哪裡放了東西了呢?其實判斷選中的面還簡單,做點小計算都不難,真正的難點在於客戶端和伺服器之間的傳輸協議。
傳輸協議,顧名思義,就是兩者之間關於信息傳輸所商定的協議。具體的技術細節在wiki.vg這裡有詳細說,我就不展開了。總結一下,就是:MC裡面除了區塊(chunk)的傳輸是全局的以外,其他各種東西的傳輸協議都是個管個的:箱子有箱子的傳輸協議,玩家有玩家的協議,熔爐有熔爐的協議。你作為玩家不管幹了什麼,改變了什麼東西,都要通過相應的協議把實際內容傳輸給伺服器端監聽這個數據的東西,然後再由它對地圖中的內容進行操作。
這裡也能看出MC的一大毛病:該耦合的不耦合,該分離的不分離,協議一大堆各自為政,各種方塊(block)又互相調用,亂成一團——一個基礎的Block類足足有幾千行。
---------------------------------------------分割線-----------------------------------------------------
說了這麼多,我們來理一下思路吧。
MC啟動你的遊戲有兩種:本地 or 遠程。
本地遊戲需要 玩家操作GUI生成或載入世界 -&> 啟動本地服務端(IntegratedServer)-&> 載入地圖 -&> 以單人單機模式提供遊戲。
或者遠程伺服器由伺服器管理員啟動(官網上有下載原版的minecraft_server.jar,但是基本上現在用的都是水桶bukkit服),然後 啟動服務端(原版是MinecraftServer) -&> 載入地圖 -&> 以多人聯機模式提供遊戲。
服務端啟動後的基本流程是: 等待玩家連接並通過驗證 -&> 發送初始數據 -&> 按需發送/載入/生成地 圖 -&> 更新(tick)地圖內容,加上散播生物啊之類的操作 -&> 發送地圖 -&> ……
而客戶端則是: 連接服務端 -&> 獲取初始數據(時間、玩家位置、驗證信息等) -&> 獲取地圖(區塊)數據 -&> 渲染地圖 -&> 接受玩家輸入(滑鼠/鍵盤操作) -&> 發送數據包給服務端 -&> 獲取更新過的地圖 -&> ……
當然理論上這裡很多步驟都應該是多線程並行進行的,但是MC的這個代碼寫的啊……很多都沒有優化,這也是為什麼MC老是卡的重要原因之一。
這大致就是整個MC裡面在發生的事情的總結,接下去的內容比較技術向,請不願看者直接跳到再下一個分割線。
-------------------------------------------分割線---------------------------------------------------
說說MC的地形生成吧。這個我研究的比較多,也想藉此機會說一說。
多圖殺貓預警!!!!!!!!!!!
更新:來填坑啦!
是時候祭出我塵封已久的mod開發工具了!
(本段內容基於1.10.2的fml反編譯,使用的是IntelliJ idea進行開發,教程。。有空再說吧)
好吧這是今年8月從fml官網上直接生成的,前面那個換電腦的時候丟了。。悲傷。。
不過這都不能阻止我的腳步!ctrl+alt+F10運行!
OK!
好吧說正事。
微軟接手之後MC開發組搞了一個功能:生成世界時進入更多世界選項(英文版More world options)
世界類型點到「自定義」(Customized),(這裡1.10.2直接點就可以了,我記得老一點的版本要按住alt還是ctrl再點才回出來),然後按「自定義」(Customize)按鈕就能調整世界生成的一些參數:
(調成中文吧。。)
可以自定義的選項有四頁,可以通過調整它們來直觀地看到MC世界生成的各種參數:
第三、第四頁比較高級,之後說到具體的生成原理的時候再說,先來玩玩前兩頁的參數:
抬高海平面,變成岩漿海,亂七八糟的東西都不要,河流越少越好,生物群系Extreme Hills+(全是山),生成!
厲害了我的哥!這裡菱形的亮區也證明了我前面說的:光照更新的時候只能更新15格以外的,更遠的哪怕有變化也不會繼續更新下去。
過一會兒就好了。可以看到MC載入地圖是載入了一個方形的區域。
遠處的樹是懸空的,因為另一邊的區塊還沒生成出來。
因為離岩漿太近,草地上的雪都化了。
按E可以賽艇。
再來一個:
泥土大理石統統不要,我就是要石頭直接暴露在陽光之下。
感覺發家致富都靠這個了。
別的統統不要,Extreme Hills生物群系,生成!
金礦與鑽石的海洋!
我要完成一個小目標了!(雖然是創造)
好吧就玩到這裡,我們來看代碼吧。
警告:接下來有大量程序猿內容,患有代碼恐懼症者可跳過。
地形生成的代碼主要在這幾個包裡面:
net.minecraft.world.gen底下的是 不同世界類型(主世界/下界/末地/超平坦)、重要的地形元素(洞穴/峽谷)、雜訊生成器(用來生成隨機的東西):
net.minecraft.world.gen.features是各種特徵,有樹(包括樹的變種針葉林之類的)、植被(仙人掌、花草之類的)、地質(黏土clay、沙子sand)、地貌(湖泊、沼澤、下界的岩漿)、人文景觀(地牢、沙井,但是村莊女巫房海底遺迹這些不在這裡):
net.minecraft.world.gen.layers其實才是生成生物群系(biome),也就是從二維上確定每個豎列(1*256*1)是什麼類別,net.minecraft.world.biome管的是生成生物群系之後確定每個生物群系的特徵。這個包裡面的東西很重要,一會兒一起說:
net.minecraft.world.biome正如前面所說是各種生物群系的內容,這個也之後單獨說:
net.minecraft.world.gen.structure管的是複雜的大型人文景觀生成(村莊,1.9之後的末地城,廢棄礦洞,下界要塞,海底遺迹):
net.minecraft.world.gen.structure.template是按照文件中定義的方塊模板來確定真正使用的方塊,就像自然生成的草地並不是千篇一律的,而是會隨機地旋轉一定的角度,就是這個東西在幫忙:
net.minecraft.village和net.minecraft.world.end是管理村莊和末地的特殊活動(和村民交♂易,打末影龍):
各個包的介紹就到這裡,接下來介紹一下生成一個區塊的整體流程,這裡我截一段net.minecraft,world.gen.ChunkProvideOverworld(生成主世界區塊)裡面的代碼來描述,加了中文注釋:
public Chunk provideChunk(int x, int z)
{
// 生成一個區塊專屬的偽隨機種子,這個種子只跟x和z的位置有關
this.rand.setSeed((long)x * 341873128712L + (long)z * 132897987541L);
ChunkPrimer chunkprimer = new ChunkPrimer(); // 封裝了一下方塊設置的操作
// 生成低解析度的生物群系、密度圖、方塊,後面會講
this.setBlocksInChunk(x, z, chunkprimer);
// 生成標準大小的生物群系
this.biomesForGeneration = this.worldObj.getBiomeProvider().loadBlockGeneratorData(this.biomesForGeneration, x * 16, z * 16, 16, 16);
// 按照生物群系替換掉高度圖裡面的普通方塊
this.replaceBiomeBlocks(x, z, chunkprimer, this.biomesForGeneration);
// 前面生成選項裡面看到的各種設置,要不要洞穴之類的
if (this.settings.useCaves)
{
this.caveGenerator.generate(this.worldObj, x, z, chunkprimer);
}
if (this.settings.useRavines)
{
this.ravineGenerator.generate(this.worldObj, x, z, chunkprimer);
}
if (this.mapFeaturesEnabled)
{
if (this.settings.useMineShafts)
{
this.mineshaftGenerator.generate(this.worldObj, x, z, chunkprimer);
}
if (this.settings.useVillages)
{
this.villageGenerator.generate(this.worldObj, x, z, chunkprimer);
}
if (this.settings.useStrongholds)
{
this.strongholdGenerator.generate(this.worldObj, x, z, chunkprimer);
}
if (this.settings.useTemples)
{
this.scatteredFeatureGenerator.generate(this.worldObj, x, z, chunkprimer);
}
if (this.settings.useMonuments)
{
this.oceanMonumentGenerator.generate(this.worldObj, x, z, chunkprimer);
}
}
// 構造區塊對象,把方塊和生物群係數據填進去
Chunk chunk = new Chunk(this.worldObj, chunkprimer, x, z);
byte[] abyte = chunk.getBiomeArray();
for (int i = 0; i &< abyte.length; ++i) { abyte[i] = (byte)Biome.getIdForBiome(this.biomesForGeneration[i]); } // 生成基礎的天空光照(見前文) chunk.generateSkylightMap(); return chunk; }
重點的setBlocksInChunk函數:
public void setBlocksInChunk(int x, int z, ChunkPrimer primer)
{
this.biomesForGeneration = this.worldObj.getBiomeProvider().getBiomesForGeneration(this.biomesForGeneration, x * 4 - 2, z * 4 - 2, 10, 10); // 這是一個低解析度的生物群系表,用在generateHeightmap裡面的
// 生成密度圖,原文雖然寫的是高度圖,但是實際上是密度圖,可能是fml的人理解有問題
this.generateHeightmap(x * 4, 0, z * 4);
// 這裡往下的這麼一大段都是在插值,密度大於0的填上石頭,低于海平面而且不是石頭的
// 填上海水,別的都是空氣。
// 話說這段真是亂七八糟,一堆magic number,估計fml的人也懶得看懂,
// 既沒加註釋也沒改變數名。。。
for (int i = 0; i &< 4; ++i)
{
// 為什麼是33和5呢? 33=32+1, 5=4+1
int j = i * 5;
int k = (i + 1) * 5;
for (int l = 0; l &< 4; ++l)
{
int i1 = (j + l) * 33;
int j1 = (j + l + 1) * 33;
int k1 = (k + l) * 33;
int l1 = (k + l + 1) * 33;
for (int i2 = 0; i2 &< 32; ++i2)
{
double d0 = 0.125D;
double d1 = this.heightMap[i1 + i2];
double d2 = this.heightMap[j1 + i2];
double d3 = this.heightMap[k1 + i2];
double d4 = this.heightMap[l1 + i2];
// 於高度y方向線性插值
double d5 = (this.heightMap[i1 + i2 + 1] - d1) * 0.125D;
double d6 = (this.heightMap[j1 + i2 + 1] - d2) * 0.125D;
double d7 = (this.heightMap[k1 + i2 + 1] - d3) * 0.125D;
double d8 = (this.heightMap[l1 + i2 + 1] - d4) * 0.125D;
// 於x和z方向線性插值
for (int j2 = 0; j2 &< 8; ++j2)
{
double d9 = 0.25D;
double d10 = d1;
double d11 = d2;
double d12 = (d3 - d1) * 0.25D;
double d13 = (d4 - d2) * 0.25D;
for (int k2 = 0; k2 &< 4; ++k2)
{
double d14 = 0.25D;
double d16 = (d11 - d10) * 0.25D;
double lvt_45_1_ = d10 - d16;
for (int l2 = 0; l2 &< 4; ++l2)
{
if ((lvt_45_1_ += d16) &> 0.0D)
{
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, STONE);
}
else if (i2 * 8 + j2 &< this.settings.seaLevel)
{
primer.setBlockState(i * 4 + k2, i2 * 8 + j2, l * 4 + l2, this.oceanBlock);
}
}
d10 += d12;
d11 += d13;
}
d1 += d5;
d2 += d6;
d3 += d7;
d4 += d8;
}
}
}
}
}
感覺這一段還要解釋一下。
x和z不是16,32這樣的世界坐標,而是區塊坐標,是世界坐標的16/1,也就是區塊(1, 2)代表了世界坐標上面的(16,32)到(31,47)的矩形區域。
這裡首先生成了一個低解析度的生物群系,長寬是10*10,是因為接下來要用的密度圖長寬是5*5。
然後生成密度圖,這個待會兒也會講。之所以說是密度圖而不是高度圖是因為它是三維的5*33*5,而不是二維的結構,所以只能稱之為密度。
接下來是線性插值。一個區塊是16*256*16,而為了讓密度更為平滑,這裡把密度圖的長寬各放大4倍,高度放大8倍,再做線性插值就會比較光滑不會變化特別突兀。
有人要問,5*4不是20嗎?33*8不是264嗎?其實因為5*33*5多出來的一圈是不用的,而且插值需要兩個值才能計算,所以四個採樣點其實需要5個數據,33也一樣是這個道理。
再淺顯一點來說,密度圖裡面的0,1,2,3,4(java數組從0開始)對應的是區塊x或z軸裡面的0, 4, 8, 12, 16,既然我們要計算位置為13,14,15的方塊,那就得用12和16對應的3和4來插值。
插值的過程從循環變數為j2的那個循環開始,0.125就是8分之1,因為高度上放大8倍,後面的0.25也是因為放大了4倍,lvt_45_1其實可以寫得易懂一點的,也是一個插值變數,跟前面的d1,d2,d3,d4,d10,d11是一樣的,只不過mojang的程序猿為了裝逼特地寫了這麼個東西罷了。。所以說代碼質量差不僅在於架構,還在於其中各種細節的寫法,像這種絕對就不算優秀的代碼。
先到這裡吧。。還有好多。。各位先看著哈。。
--------------------------------------------分割線-------------------------------------------------
總結全文,深化主旨,首尾呼應:
Minecraft是個好遊戲,但代碼寫的確實不咋地。
我最初接觸MC還是13年,那時候大部分還在用1.4.2或者1.5,當然,都是盜版。
一開始只是玩單機,打生存,搭房子,有時候開創造玩玩紅石電路,特別喜歡一個人晚上獨自邊打MC邊聽他的bgm。C418的歌,很寧靜,讓人很放鬆。
後來基本上大部分玩法都玩兒遍了,也不怎麼願意去花功夫做那種超大的紅石電路或者建築之類的,總想著找點別的花樣。於是我裝了Forge,下了很多mod,比如IndustrialCraft這種大名鼎鼎的,還有一些輔助性的mod,比如1.6.2之後就停止開發的SPC(Single Player Command 賣安利)。SPC是模仿WorldEdit的擔任命令行mod,就是各種批量編輯,一個人搭建築非常方便(因為WorldEdit WE只有多人能用,SPC可以單人模式用)。結果換1.7.2之後SPC不支持了,於是就萌生了自己開發一個的想法。(好吧最後還是流產了,當時水平不夠做出來的性能很差,現在有能力做了又沒了當初那種激情)
又因為剛好學java,聽說MC也是用java寫的,所以就網上找教程寫mod。我還記得14年的時候架著100K不到的梯子屁顛屁顛地裝fml的時候,去maven central倉庫上下東西動不動就停住了,結果一晚上都沒裝好。
國內(哪怕到今天)都缺少真正有質量的mod開發教程,特別是中文的基本沒有。貼吧上曾找到過一篇講mod開發環境配置的,不過也止於1.6.2,到了1.7之後FML的整個API變了好多,原來的教程都用不了了。
後來在網上找到了一篇文章:http://notch.tumblr.com/ ,是MC最早的開發者Markus Persson(馬庫斯·泊松,網名notch)的個人博客,還有一篇講述他的地形生成演算法的博文:
http://notch.tumblr.com/post/3746989361/terrain-generation-part-1
大概翻譯一段(水平有限):
In the very earliest version of Minecraft, I used a 2D Perlin noise heightmap to set the shape of the world. Or, rather, I used quite a few of them. One for overall elevation, one for terrain roughness, and one for local detail. For each column of blocks, the height was (elevation + (roughness*detail))*64+64. Both elevation and roughness were smooth, large scale noises, and detail was a more intricate one. This method had the great advantage of being very fast as there』s just 16*16*(noiseNum) samples per chunk to generate, but the disadvantage of being rather dull. Specifically, there』s no way for this method to generate any overhangs.
在Minecraft最早的版本中,我將一張2維perlin noise圖作為高度圖來確定世界的形狀。或者說,好幾張perlin noise圖。一張是整體海拔,一張是地形的粗糙度,一張是小範圍的細節。對於每一列方塊,其高度是 (海拔+(粗糙度*細節))*64+64。 海拔和粗糙度的圖都是連續光滑的,大尺度的雜訊圖,而細節則更加參差不齊。這個生成方式有一個巨大的優點就是奇快無比,因為對於每個區塊,只不過需要生成16*16*(雜訊圖的數量) 個採樣點,然而缺點就是地圖相當單調無趣。更重要的是,這種方式根本生成不了懸崖。
So I switched the system over into a similar system based off 3D Perlin noise. Instead of sampling the 「ground height」, I treated the noise value as the 「density」, where anything lower than 0 would be air, and anything higher than or equal to 0 would be ground. To make sure the bottom layer is solid and the top isn』t, I just add the height (offset by the water level) to the sampled result.
於是我改成了一種有些相似的、基於3維perlin noise的生成方式。我並不是對「地面高度」進行採樣,而是將這個雜訊的數值看做是「密度」(因此我說前面代碼中應該是密度圖而不是高度圖 ——譯者注),其中密度小於0的點都是空氣而大於等於0的會成為大地(其實就是實心方塊 ——譯者注)。為了確保地圖底端是地面而頂端是空氣,我便把採樣值加上採樣點的高度(海拔高度)。(其實應該說是「減去採樣點的高度」更易懂,因為大於0的才是方塊,而水下的高度是負值 ——譯者注)
Unfortunately, I immediately ran into both performance issues and playability issues. Performance issues because of the huge amount of sampling needed to be done, and playability issues because there were no flat areas or smooth hills. The solution to both problems turned out to be just sampling at a lower resolution (scaled 8x along the horizontals, 4x along the vertical) and doing a linear interpolation. Suddenly, the game had flat areas, smooth hills, and also most single floating blocks were gone.
不幸的是,我立刻碰上了可玩性和效率兩方面的麻煩。因為有大量的點需要被採樣所以效率很成問題,又因為缺乏大片的平地或平滑的山丘而缺乏可玩性。這兩個問題共同的解決方案最終定為了這樣:用一個更低的解析度去採樣雜訊圖(橫向8倍,縱向4倍)(代碼裡面是橫向4倍縱向8倍,可能跟後來的代碼改動有關 ——譯者注)並且進行線性插值(見setBlocksInChunk函數 ——譯者注)。突如其來地,世界上有了平原、丘陵,而且單獨浮在空中的詭異方塊中的絕大部分也都消失無蹤了。(其實還有很多了啦 ——譯者注)
這激起了我濃厚的興趣。於是我把長長的代碼列印下來,8號字體,一面3列,足足十幾張紙,每天一有空就看代碼,讀代碼,用筆做注釋,並且為我覺得命名不當的部分想一個更好的名稱。到現在這些還藏在我的書桌裡面。這些東西百度根本搜不到,google也很少有結果,而以fml對這些代碼的注釋程度來看他們根本懶得去讀這些既混亂又不必要讀懂的代碼。
當然後來還是不了了之了。我自己也仿造過MC,當時就是完全追星似的瘋狂模仿,用的java,跟MC一樣的LWJGL庫,同樣(好吧我承認這麼做有盜版的嫌疑)的紋理,幾乎相同的文件格式、區塊大小、渲染原理、(基於notch文章的)地形生成、體系架構,幾乎就是我前面寫的這麼多的一個自己的重現。我甚至還學習MC混淆了編譯後的代碼。可笑的是,迫於當時的水平,效率竟比MC原版還要低。最後開發進程是因為我U盤的丟失而終止的,源碼自然也丟了,只剩一份速度奇慢的編譯版本,下載地址:http://pan.baidu.com/s/1eQvM0U2,有興趣的自己下載來玩玩吧。
今年想過要重製,結果忙了一個夏天又泡湯了,現在想來總覺得有點可惜。
後來被朋友忽悠著去打了一段時間MC伺服器,也買了正版,不過伺服器沒有打過一個月的,基本兩三個星期就崩掉關服不開放了,也不知道是我的問題還是整個現狀都這樣。(我真的沒有干過壞事啊!)據說國內開一個服很快就會有黑客來勒索要錢否則就DDoS攻擊(Distributed Denial of Service分散式拒絕服務攻擊,用大量肉雞的流量淹沒對方,讓對方或因為過高的流量費用而關閉服務,或因為過低的響應速度而降低服務質量到一個不可接受的程度),也不知道是不是真的,反正崩了好幾個服,打拚好久的家園東西都沒了,也就不打了。
最近確實玩的不多,也就偶爾休息的時候像以前那樣,搞搞建築,搭搭紅石電路之類的,都是單機玩,一方面沒空,一方面也有點享受那種獨自一人的感覺和C418的背景音樂。
我知道這段算不上問題的「從編程的角度來看」,不過寫了前面這麼多,也使我回想起來以前發生過的不少。要不是這個答案,也不知道這段往事會被封存到什麼時候。就藉此機會,一吐為快吧。希望大家不必計較,並且歡迎任何技術上的討論。
以上。
各位看官看完記得點個贊再走啊。。。謝啦。。。
當時Minecraft要移植到win,可沒少找dx的人幫忙。一看他們gles的用法都快吐血了。全部glVertexPointer直接給數據,不慢才怪。問他們為啥不用vertex buffer,他們的回答是,「什麼是vertex buffer」。水平可見了吧。
一定會有人問,在android的低端gpu上為啥minecraft也能跑流暢?原因在於它太流行了,以至於各家的android驅動都專門針對它有個code path,特別優化了glVertexPointer到GPU的通路,在裡面模擬了一個vertex buffer的實現。看了上面幾個答案……嘛,聽說你們喜歡吐槽?
先說說我在組裡幹啥:地形演算法,方塊效果,GUI。所以實體我沒有什麼發言權。
1.整體架構
對於modder來說,寫Mc mod的的時候,我總是想著Java怎麼就不提供個直接能覆蓋掉MC原類的關鍵字呢?Mc源代碼在部分層面的邏輯非常混亂,後面慢慢吐,不急。Mc的混亂不在於不同程序員間的代碼風格迥異(當然也是因素之一),更在於Mc與他的「歷史遺留問題」。打個比方說,一個孩子在搭積木,他開始用了方形的結構磕磕絆絆的搭了好幾層,後來,他發現三角形結構更加穩定。然而他那時偷了點懶,在方形的基礎上構造一層層穩固的三角形。積木越搭越高,卻也越來搖搖欲墜。當孩子望著這些積木打算著手修改時,卻發現問題早就樹大根深了。Mc就是這樣,Notch早期很明顯的以小項目為基礎考慮而構建的代碼、邏輯結構很大程度上或多或少禍害了如今的Mc。不是說Notch開始不對,是說Mc在還來得及的時候沒有痛下決心重寫項目。後來的程序中,當然不乏漂亮的邏輯,但是這都有一個蹩腳的點為根基。從根本上講,Mc「根本」不行。由於當初小項目開發的前瞻性不足,如今留給mod開發者抑或是Mojang的開發空間十分狹隘。得虧有了ASM得以使開發者在源碼上鑿開空間。
2.Truck
你你你……我我我……唉:-(!
Mc效率差的原因之一。這樣吧,這部分我先靜一靜,有機會說說哈。
3.繪製
有答案已經提了,直接給數據什麼的……不提效率,反問Mojang團隊自己看不看得懂自己在寫什麼!
4.邏輯
為什麼一個方塊有4種得到掉落物的方法,還附贈一個掉落物品的方法?為什麼縱使每種物品方塊幾乎都有class,指定他們的硬度等參數還要在init里?這麼說吧,我植物這方面做的比較多,如果你的植物不屬於換了材質的小麥,基本就是要繼承Block再造輪子了。沒辦法,原版植物誰用誰知道。
5.GUI
又要造一波輪子。個人想法:mc的GUI本身的滑鼠部分寫的太次了!完全沒有繼承價值,屬於重載了super都不帶一句那種。自帶的GuiButton就是個擺設。
6.硬編碼
Mojang喜歡硬編碼跟見了親人一樣。比如物品Id、方塊Id、子物品、RenderType……分配一個,用registry很難嗎?
/==================
專門來一篇Minecraft的介紹。先聲明,這裡只是普通的Moder。
1.Minecraft的地圖生成演算法
Minecraft的地形演算法是基於Perlin Noise的2-pass過程。關於Perlin Noise的,可以看看git上我寫的版本(鏈接:https://github.com/kaaass/JavaPerlin 直到目前尚未完成)。第一次:基本生成,確定biome,建立基礎地形。第二次:特性生成,從layout開始(河流等等),然後是洞穴、樹、村莊什麼的。由於存在先後多次生成,就會偶爾遇到村莊位於峽谷上等等奇葩景觀。
2.Minecraft的Block
方塊具有很多特性,這裡只講一點。先是metadata,諸如植物(單指Corp)不同的生長狀態都是不同的metadata決定的。TileEntity,entity是實體,諸如玩家、怪物都屬於entity。metadata的存儲數據量對部分方塊,比如箱子。所以引入了TileEntity的形式。暫時就說辣么多。
3.物品
物品具有和block相似的機制。存儲狀態使用damage值決定。沒錯很多時候物品就是用名字上叫「耐久」的值存儲狀態的。然後是subitem的機制,就是子物品。比如染料(dye),染料很多,然而其實物品id是一樣的。
我也看了一點點,說一些大神認為嘗試的東東…
mc核心貌似是一個每秒20tick的線程,然後需要不斷執行的(比如紅石,小麥啥的)註冊到這個線程里。
這樣的後果是…伺服器人多時每秒執行不完20tick,然後時間被拉長…燒個鐵都要半天233333。伺服器還有工業觸做UU的,都是整夜整夜的掛機呀,跟掛qq似的。
另外,mc的光線(不是光影mod看到的光線)永遠是是從上往下的,因為判斷殭屍自然的代碼,有一個就是判斷殭屍正上方有木有方塊,因此…即使是255層的方塊,下面殭屍照樣不會自燃……
不過,mc剛開始畢竟是一個人寫的(即使到現在也沒幾個人好像…)。從設計到編碼,從存儲到測試,算是一個人乾的…雖然mc最初的版本非常簡陋,但也很了不起了…畢竟不能什麼都考慮到。膜拜下…地圖生成:基於分形的方法
地圖更新:元胞自動機
地圖使用chunks,指針八叉樹。
地形生成使用Perlin Noise。
渲染直接用lwjgl調用OpenGL。
我發現大部分答主都是馬後炮:從結論倒推做法。這種回答方式很不好,一來這是授之以魚的方式,二來已經做好的Minecraft比較複雜,不適合新手學習。
無論是做遊戲,還是做程序,本質上是拿到需求後,先數據建模,然後把數據處理流程設計好。如果你想做Minecraft這類東西,可以找一本靠譜的遊戲設計入門教材,然後從類似於坦克大戰、飛機大戰之類的簡單遊戲開始,一步一步的學:
1.先做一個程序,能控制飛機移動。
2.做一些能隨機移動的NPC飛機。
3.設計飛機能發射簡單的子彈,並實現子彈傷害。
4.使用簡單的同步策略讓程序網路化,增加多人同屏遊戲功能。
5.增加遊戲背景。
6.增加可破壞物件。
......
這樣一步一步來,你就能掌握以及清楚大概的遊戲設計方法。遊戲領域裡,有些問題,要做的比較好,非常費時,比如良好用戶體驗的控制、精確的同步等等。這些問題,先用最簡單的策略代替,不要鑽牛角尖,浪費時間。把整個設計流程把握後,再選擇自己喜歡的細分方向去進行深究。唔,可以這樣想,每個方塊可以用一個向量表示。
(獨立標識,材質相關,透明相關,半磚相關,……)
然後把這些方塊按照標識堆疊起來,這樣,遊戲的場景就搭建起來了。然後根據表面的方塊生成碰撞、光照。
由於流水、岩漿很耗內存,估計是用遞歸寫的。
聽我說得很容易,實際做的時候會遇到各種各樣的細小問題需要你解決,也不是什麼容易的事情呢。
mc本質上是用java做的,其實就會搭積木,越到高層越抽象的積木。你要想知道積木怎麼搭建的,你要自己去玩玩積木。
我做了一個插件製作的教程,如果您感興趣可以用java感受一下Minecraft,這個連接到b站:
[oeasy]我的世界生存實況mc編程入門(合集),minecraft(mc新玩法)我的世界伺服器插件開發,MC創作的新_野生技術協會_科技_bilibili_嗶哩嗶哩。
還有一個mod製作的教程, [oeasy]我的世界生存實況 mc(minecraft)創作 mod開發--forge1.8.9,我的世界開發伺服器
紙上得來終覺淺,須知代碼要恭行。從代碼角度是最根本、最適合、最實際的方式了解mc是怎麼設計的。
推薦閱讀:
※什麼樣的VR空間會引起用戶視覺上的不適?
※當前的國產遊戲有哪些共同的弱點?
※關於即將去美國讀遊戲設計方面master的選校問題?
※為什麼有些人玩遊戲充很多的錢?
※桌游在遊戲類型上大致分為哪幾類,有哪些代表作?
TAG:遊戲設計 | 編程 | Java | 我的世界Minecraft |