這篇文章收集了這四年間被問到的各種問題,因此如果你遇到問題的話,不妨現在這裡找找,看看有沒有已有的解決方案.
目前離完成還尚早,會不斷更新...
目錄:
Forge安裝與開發環境配置
-ForgeGradle配置慢/配置失敗
-在decompileMc或recompileMc時出錯
-更換Gradle位置
-Eclipse切換工作目錄後是個空的項目
-所謂的"Using incremental CMS..."
-在開發環境下無法啟動服務器
-中文目錄/帶有空格的目錄
-IDE項目缺少必備的庫
-啟動時出現java.lang.reflect.InvocationTargetException
磚塊
-父類構造函數未定義等構造函數錯誤
-磚塊實時更新,實時處理磚塊發生的變化
-創建一種液體
-創建每面材質不同的磚塊
-設定磚塊在地圖上的顏色
-1.8的BlockState和Property系統
實體
-實體的額外數據與持久化
世界
-世界的額外數據與持久化
-獲取一條線段上最近的磚塊
-生成粒子特效
-自定義粒子特效
配方
-刪除已有的合成與冶煉配方
GUI
-在GUI界面中渲染玩家
事件總線
-註冊Forge/FML事件總線時出錯
-註冊事件無效,監聽方法不被調用
-總共有多少個事件總線
-為什麼有兩套事件總線(FML的和Guava的)
-為什麼Forge和FML有獨立的事件總線
-FML的事件總線能處理哪些事件
-FML的Guava事件總線能處理哪些事件
Coremod
-如何查詢全混淆名和半混淆名
Forge安裝與開發環境配置
ForgeGradle配置慢/配置失敗
考慮使用FGOW.
不過Gradle本體下載(具體表現是第一次在cmd運行Gradle時顯示形如"Downloading http://services.gradle.org/distributions/gradle-X.X-XXX.zip"的字樣)的速度慢是無解的,目前國內唯一的Gradle本體的鏡像只有bangbang93的BMCLAPI2提供了2.0版的鏡像,啟用方法是編輯forge目錄下的gradle/wrapper/gradle-wrapper.properties,將其中的
distributionUrl=http\://services.gradle.org/distributions/gradle-2.0-bin.zip
改為
distributionUrl=http\://bmclapi2.bangbang93.com/gradle-2.0-bin.zip
其他版本的Gradle目前還沒有鏡像,也許我有時間會往FMM上傳個鏡像...
在decompileMc或recompileMc時出錯
這個一般是由於內存不足所導致,出錯信息多包含有"java.lang.OutOfMemoryError: Java heap space"之類的.解決方法是用文本編輯器打開gradlew.bat(Windows下)或gradlew(OSX或Linux),在"set DEFAULT_JVM_OPTS="的後面加上-Xmx1000m,這個是手動設置最大1000M內存,通常來說是夠用了,如果還不夠的話可以繼續調大.
更換Gradle位置
在Win7/8/8.1中,Gradle默認是把庫下載在C:\Users\[用戶名]\.gradle中,如果要修改默認位置的話,在環境變量的系統變量里,添加項GRADLE_USER_HOME,內容填新的位置.
在更換目錄後,你可能需要輸入gradlew.bat inti來重新修復一下.
Eclipse切換工作目錄後是個空的項目
大概是配置沒完成吧...在setupDecompoWorkspace後別忘了再執行eclipse任務,讓Gradle為項目生成內容.
所謂的"Java HotSpot(TM) 64-Bit Server VM warning: Using incremental CMS is deprecated and will likely be removed in a future release"
很多人都說在啟動遊戲時遇到了這個錯誤結果進不去遊戲,儘管我從來沒遇到,但我相信所有的錯誤肯定都不是因為它的原因...它只是因為Java8將CMS回收器設定為"不推薦使用",而Forge的默認MC啟動配置是使用CMS回收器所導致.這肯定不是導致出錯的原因,然而由於某些奇妙的原因,這句話往往是在控制台的最後一句話輸出,因此很多從不讀文字的人誤將它認為是Bug原因.
如果你是使用Forge1.7.2和Java8的話,那就把Forge升級到1.7.10或1.8,或者把Java降級到Java7.
在開發環境下無法啟動服務器
看看控制台信息在閃退前最後幾行有沒有輸出"You need to agree to the EULA...",有的話打開Forge目錄下的eclipse/eula.txt,將eula=false改成eula=true.
中文目錄/帶有空格的目錄
盡量避免目錄中帶有中文或空格,同樣的盡量避免Mod文件的文件名中包含非Ascii字符(文件內含有非Ascii字符沒關係,只要文件編碼正確即可).
IDE項目缺少必備的庫
正常情況下ForgeGradle在配置過程中會下載或生成(比如反混淆後的Minecraft)必須的庫,但在少數情況下仍會發生"Project 'Minecraft' is missing required library"這樣的缺庫問題,主要是這三種情況引起:
- 正確下載或生成了庫,但在生成IDE項目時沒有正確加載庫:這種情況下可以執行cleanEclipse/cleanIdea清理項目文件,然後再執行eclipse/idea重新生成IDE項目.還不行的話可以嘗試手動指定庫的位置...
- 生成庫失敗:forgeSrc/forgeBin和start這三個庫是由ForgeGradle在配置項目時生成的,這種情況下就得考慮從頭開始重新配置一遍了...
- 下載庫失敗:Gradle確實有如果下載庫失敗時就會靜默地跳過然後在生成IDE項目時給你留個缺失庫的特性...然而這在ForgeGradle不太可能出現,因為ForgeGradle在生成項目時的其中一步就是重新編譯MC,如果缺庫的話會立刻發現的.
啟動時出現java.lang.reflect.InvocationTargetException
看一下下面的Caused by是不是"java.lang.UnsatisfiedLinkError: no lwjgl in java.library.path",如果是的話則是JNI沒有設定到正確的位置.
通常來講ForgeGradle在配置時會自動設置項目引用的JNI庫的位置,然而那東西你懂的...很多時候只是勉強JustWork™.可以嘗試以下手動修復方式:
對於Eclipse,打開你的項目的項目屬性,在Java Build Path - Libraries中,隨便找一個庫(比如forgeSrc),點開它前面的白箭頭將它展開,雙擊Native Library Location,將目錄設定到"C:/Users/[用戶名]/.gradle/caches/minecraft/net/minecraft/minecraft_natives/[MC版本]",然後應該就可以了,如果你找不到那個目錄的話,說明你配置失敗了...重新配置一遍開發環境吧.
IDEA或Netbeans也同理,找各自的模塊/項目配置,將一個庫的JNI目錄設到那個文件夾即可.
磚塊
父類構造函數未定義等構造函數錯誤
使用/繼承了錯誤的Block類,你在開發環境下能看到起碼六種Block類,正確的Block類是net.minecraft.block包下的類.
磚塊實時更新,實時處理磚塊發生的變化
首先你要明白自己是在做什麼,服務器的地圖中你的磚塊可能有少至幾十塊,多至幾千塊,如果你希望系統實時處理它們,你得知道服務器面臨著多大的壓力.
磚塊有兩種更新方式,被動更新和主動更新.
被動更新是指臨近磚塊發生變動時引發,比如當你調用World類的notifyBlocksOfNeighborChange方法可以讓某點的上下左右前後6個方向的臨近磚塊進行被動更新,也可以調用notifyBlockOfNeighborChange來讓某個點的磚塊進行被動更新.被動更新調用的Block類方法是onNeighborBlockChange,用它你可以處理諸如紅石信號變化,或者臨近磚塊的破壞等事件.
主動更新則是每隔一段時間固定引發一次.主動更新都是調用Block類的updateTick方法,但更新時間間隔有兩種,一個是永久性隨機更新,即間隔隨機的一段時間時進行更新,啟用隨機更新的辦法是在初始化磚塊時調用setTickRandomly(true),隨機更新適用於那些對更新頻率要求不高的,比如農作物什麼的;另一種更新則是一次性定時更新,通過調用World類的scheduleBlockUpdate或scheduleBlockUpdateWithPriority來讓遊戲在指定幀間隔後調用某點磚塊的updateTick,在調用完之後,遊戲就不會再調用了,除非你手動再申請一次,這種更新適合像按鈕之類的會一次性定時發生變化的磚塊;如果你在updateTick中申請定時更新的話,那這就變成永久性的固定間隔更新了,適用於像流體那種需要頻繁更新的東西.
創建一種液體
Forge提供了一套流體系統,具體見http://www.minecraftforge.net/wiki/Create_a_Fluid,Wiki上難得一篇寫的好的文章...我不覺得我能寫的比它更清楚.
創建每面材質不同的磚塊(即多材質磚塊)
1.7:最簡單的是像書架那樣與方向無關的多面磚塊,只要重寫磚塊類的getIcon,根據不同面(第一個參數)來返回不同材質,然後再重寫registerBlockIcons來加載多個材質就行.比如:
public class BlockSkybox extends Block { private IIcon[] icons; public BlockSkybox() { super(Material.rock); setBlockName("SkyboxBlock"); setCreativeTab(CreativeTabs.tabBlock); //因為下面我們是手動加載材質,所以就不需要setBlockTextureName了 } @Override public IIcon getIcon(int face, int meta) { return icons[face]; } @Override public void registerBlockIcons(IIconRegister iIconRegister) { icons = new IIcon[6]; icons[0] = iIconRegister.registerIcon("examplemod:csskybox_down"); icons[1] = iIconRegister.registerIcon("examplemod:csskybox_up"); icons[2] = iIconRegister.registerIcon("examplemod:csskybox_north"); icons[3] = iIconRegister.registerIcon("examplemod:csskybox_south"); icons[4] = iIconRegister.registerIcon("examplemod:csskybox_west"); icons[5] = iIconRegister.registerIcon("examplemod:csskybox_east"); } }
而如果要實現像熔爐或活塞那樣有方向的多材質方塊就有點複雜,首先是要判斷磚塊該朝向哪個方向,BlockPistonBase類的determineOrientation方法可以用來判斷合適的朝向,返回的數值即磚塊正向所在的面;然後是數值的存儲和讀取,這個可以用metadata來完成.下面給出一個不完善的例子,不完善的地方在稍後解釋:
public class BlockKokoro extends BlockDirectional { private IIcon forward, left, right, top, other; public BlockKokoro() { super(Material.iron); setBlockName("KokoroBlock"); setCreativeTab(CreativeTabs.tabBlock); } @Override public void onBlockAdded(World world, int x, int y, int z) { //通過setBlock放置的磚塊 world.setBlockMetadataWithNotify(x, y, z, 2, 1 | 2); //一個更複雜的機制是判斷附近哪個方向沒有其他磚塊,然後朝向那裡.這裡我們簡單地讓磚塊朝北. } @Override public void onBlockPlacedBy(World world, int x, int y, int z, EntityLivingBase entity, ItemStack itemStack) { int meta = BlockPistonBase.determineOrientation(world, x, y, z, entity); world.setBlockMetadataWithNotify(x, y, z, meta, 1 | 2); //1|2的來歷見World類的setBlock } @Override public IIcon getIcon(int face, int meta) { switch (meta) { case 0: //朝下 face = Facing.oppositeSide[face]; case 1: //朝上 switch (face) { case 0: return other; case 1: return forward; case 2: return top; case 3: return other; case 4: return right; case 5: return left; } break; case 2: //朝北 face = Facing.oppositeSide[face]; case 3: //朝南 switch (face) { case 0: return other; case 1: return top; case 2: return other; case 3: return forward; case 4: return right; case 5: return left; } break; case 4: //朝西 face = Facing.oppositeSide[face]; case 5: //朝東 switch (face) { case 0: return other; case 1: return top; case 2: return left; case 3: return right; case 4: return other; case 5: return forward; } break; } return other; } @Override public void registerBlockIcons(IIconRegister iIconRegister) { forward = iIconRegister.registerIcon("examplemod:kokoro_f"); left = iIconRegister.registerIcon("examplemod:kokoro_l"); right = iIconRegister.registerIcon("examplemod:kokoro_r"); top = iIconRegister.registerIcon("examplemod:kokoro_t"); other = iIconRegister.registerIcon("examplemod:kokoro_o"); } }
這個跟上一個相比主要是多了個onBlockPlacedBy和onBlockAdded,用來判斷放置磚塊時的朝向,然後在getIcon里根據渲染面和磚塊朝向的關係來決定材質.這個方案的不完善之處在於材質不會旋轉,當正面朝上或朝下時問題尤為明顯,在1.7下解決方案恐怕只有將側面材質設計為與方向無關的對稱圖形,或者為側面材質額外製作其他的版本,比如左旋和右旋版本.
1.8:
待補充,如果我過了很久都忘了寫的話儘管spam我!
設定磚塊在地圖上的顏色
磚塊在地圖上的顏色由它的材質決定,材質的構造函數中的參數就是地圖顏色,遺憾的是遊戲似乎不支持自定義顏色...
1.8的BlockState和Property
這部分其實應該放在磚塊章節,然而由於教程目前尚未全面更新到1.8,因此現在這裡寫一下.
在1.8之前,我們如果想在磚塊中存儲額外信息的話只能使用4位的Metadata.而從1.8開始,Minecraft提供了BlockState系統.
BlockState系統的總體思想是提供一種更好地表達一個磚塊的狀態的方案,這個方案既要能直白地表達要存儲的狀態;又要足夠的小,不至於讓內存或硬盤爆掉;同時也要足夠的快.最終產物就是BlockState.
原版BlockState系統(Forge對它做了擴展,這個以後有時間再寫)主要由這三部分組成:
BlockState類:描述一種磚塊(即一個Block類的實例)所有可能的狀態.
IBlockState接口:代表一種狀態.
IProperty接口:代表狀態中的一種屬性.
我們以1.8的石頭磚塊的BlockStone類為例來解釋各部分的作用和使用方法.
首先它先聲明了一種屬性:
public static final PropertyEnum VARIANT = PropertyEnum.create("variant", BlockStone.EnumType.class);
從代碼中可以很直觀地看到VARIANT是一種枚舉類型的屬性,PropertyEnum繼承自PropertyHelper,而PropertyHelper則實現了IProperty.Minecraft默認提供了這三種屬性類型的實現:連續整數(PropertyInteger),枚舉(PropertyEnum)和布爾值(PropertyBool),其中枚舉還有一個專門用於描述方向的封裝PropertyDirection.
然後我們要聲明BlockState,這部分的代碼在稍微靠下的地方:
protected BlockState createBlockState() { return new BlockState(this, new IProperty[] {VARIANT}); }
它重寫了基類的createBlockState,聲明了一個擁有VARIANT屬性的BlockState,它的構造函數會自動計算所有屬性能組成的全部狀態,用數學的說法是求它們的笛卡爾積,比如如果一個BlockState有一個取值範圍在0~2的連續整數屬性和一個布爾值屬性,那麼它就有6種狀態,分別是{0 False, 0 True, 1 False, 1 True, 2 False, 2 True}.
我們之前提到IBlockState接口的實例代表一種狀態,首先我們先說明如何修改狀態,修改狀態是通過IBlockState的withProperty方法,這個方法的參數是給定一個待更新屬性和它的待更新值,返回值是更新後的狀態,還是以剛才的為例,假如那個整數屬性叫INT,那麼要將一個值為{0 False}的狀態改成{2 False}的話,代碼是:
IBlockState state = oldState.withProperty(INT, Integer.valueOf(2)); //oldState為舊狀態,state為修改值後的新狀態
需要注意的是,withProperty不會對舊狀態產生任何影響,上述代碼在運行完後oldState不會有任何改變,因此你永遠無需擔心調用withProperty會破壞舊狀態,你可能會認為Minecraft默認的IBlockState實現採用的是不變量(Immutable)設計,即每次修改值時始終返回一個新實例而不會改變原實例,這並不是完全正確的,實際上是BlockState已經預先生成好了所有可能的狀態,每次調用withProperty時只是根據一個狀態轉移表查找到應轉移到的狀態,然後返回那個狀態的實例.
通過withProperty,我們可以將任何一個已有狀態修改成我們想要的狀態,但是我們該如何獲取"第一個"狀態,也就是如何憑空獲取一個IBlockState實例? BlockState提供了getBaseState方法可以獲取一個空的狀態,然而這個空狀態的值是不確定的,每次手工賦值必然很麻煩,因此Block類允許配置一個默認狀態,設置默認狀態的辦法是調用Block類的setDefaultState,以BlockStone為例:
this.setDefaultState(this.blockState.getBaseState().withProperty(VARIANT, BlockStone.EnumType.STONE));
這個代碼是設置該磚塊的默認狀態為{STONE},以後如果要獲取默認狀態,直接調用Block類的getDefaultState方法即可.
最後我們要考慮的是如何保存狀態,現在我們又要和老朋友Metadata打招呼了,MC1.8仍然使用Metadata系統,並且也依然受最多16種取值的限制,所以說你的BlockState雖然最多可能有超過16種狀態,但在保存時必須只保留最關鍵的屬性,將它簡化到16種以下,比如說如果你設計了一種管道磚塊,它有2種屬性,一個有16種取值的用於標識管線類型的枚舉屬性,和一個範圍在0~63用於標識管道與周圍連接情況的整數屬性(如果你好奇0~63是怎麼來的話...使用6個二進制位來標識這個磚塊與周圍6個方向的連通情況),顯然它們的笛卡爾積有多達1024種情況,然而我們可以只保存標識管線類型的那個屬性,標識連接情況的屬性可以在運行時通過判斷周圍磚塊的類型來推算出來.
保存、讀取和計(bu)算(wan)非關鍵屬性的方法以及它們的使用範例分別是:
public int getMetaFromState(IBlockState state) { //返回值為該狀態的Metadata的值,不需要的話就返回0或者乾脆不重寫這個方法 return ((EnumPipeType)state.getValue(pipeType)).getMetadata(); //模仿BlockStone的寫法 } public IBlockState getStateFromMeta(int meta) { //返回值為該Metadata對應的狀態,不需要的話就返回getDefaultState或者乾脆不重寫這個方法 return getDefaultState().withProperty(pipeType, EnumPipeType.byMetadata(meta)); //模仿BlockStone的寫法 } public IBlockState getActualState(IBlockState state, IBlockAccess worldIn, BlockPos pos) { //返回值為補全了非關鍵屬性的狀態,如果不需要補全數據的話,直接"return state;"即可;或者乾脆不重寫這個方法. int i = 0; i |= (worldIn.getBlockState(pos.up() ).getBlock() == this ? 1 : 0) << EnumFacing.UP .getIndex(); i |= (worldIn.getBlockState(pos.down() ).getBlock() == this ? 1 : 0) << EnumFacing.DOWN .getIndex(); i |= (worldIn.getBlockState(pos.east() ).getBlock() == this ? 1 : 0) << EnumFacing.EAST .getIndex(); i |= (worldIn.getBlockState(pos.west() ).getBlock() == this ? 1 : 0) << EnumFacing.WEST .getIndex(); i |= (worldIn.getBlockState(pos.north()).getBlock() == this ? 1 : 0) << EnumFacing.NORTH.getIndex(); i |= (worldIn.getBlockState(pos.south()).getBlock() == this ? 1 : 0) << EnumFacing.SOUTH.getIndex(); return state.withProperty(linkingState, Integer.valueOf(i)); }
原版BlockState系統的內容便就此結束了,如果你問BlockState系統的作用的話,那就是它創造了數據驅動渲染的可能,數據驅動就是指通過外部數據來決定程序運作流程,而不是通過硬編碼的代碼,比如BlockModelShapes類中的這行代碼:
this.registerBlockWithStateMapper(Blocks.stone, (new StateMap.Builder()).setProperty(BlockStone.VARIANT).build());
這樣便建立了磚塊狀態與渲染的映射關係,遊戲會根據VARIANT屬性的值從assets/minecraft/bloackstates中選擇合適的數據文件用於渲染.
實體
實體的額外數據與持久化
調用實體的getEntityData方法可以獲取一個Forge提供的專門用於存儲自定義信息的NBT組件,服務器會自動完成它的保存和讀取.然而在多人遊戲中這些數據不會被服務器同步給客戶端,因此你可能需要藉助DataWatcher或自定義封包等方式來實現客戶端到服務器的數據同步,這個問題將在網絡章節中討論.
此外,對於玩家實體來說還有一種方式來實現持久化,就是監聽PlayerEvent.SaveToFile事件和PlayerEvent.LoadFromFile事件,這兩個是在服務器保存和讀取一個玩家的信息時發生,這兩個事件均提供了名叫getPlayerFile的方法,通過這個方法你可以讓Forge為你推薦一個指定後綴名的存儲文件位置(並不會立刻創建文件),舉例:
@SubscribeEvent public void onPlayerLoad(PlayerEvent.LoadFromFile event) { File file = event.getPlayerFile("nvm"); Date registerTime; if(!file.exists()) { ObjectOutputStream stream = null; registerTime = new Date(); try { stream = new ObjectOutputStream(new FileOutputStream(file)); stream.writeObject(registerTime); } catch (IOException e) { e.printStackTrace(); } finally { if(stream != null) try { stream.close(); } catch (Exception e2) {} } } else { ObjectInputStream stream = null; try { stream = new ObjectInputStream(new FileInputStream(file)); registerTime = (Date)stream.readObject(); } catch (Exception e) { e.printStackTrace(); registerTime = new Date(); } finally { if(stream != null) try { stream.close(); } catch (Exception e2) {} } } //把registerTime存在某處,比如一個map中... } //saves\New World\playerdata\a498be5d-0142-3d23-a17c-8823a1cd27b0.nvm
這段代碼用來記錄玩家首次登陸服務器的"註冊時間"(雖然這用NBT能更好地完成).遊戲會在玩家進入遊戲時檢查存檔中的playerdata目錄下有沒有名為"[玩家的uuid].nvm"的文件,如果沒有的話,則會創建一個並寫入玩家的登陸時間;有的話則會讀取.
這段代碼沒有考慮文件寫入的原子性,如果文件較大,或者純粹是運氣不好的話,可能在你向磁盤中寫入文件時恰好趕上服務器崩潰、電源掉電、機櫃被維修工當成空調拉走等各種讓程序中斷的情況,如果此時正巧你在向硬盤中寫入數據的話,文件就損壞了,可以想象當重新上線的玩家看到他的數據一夜回到解放前時會有多麼憤怒,因此如果你要保存的文件較大的話,應當考慮先輸出到臨時文件當中,在輸出完畢後刪掉舊文件並重命名臨時文件.
如果你是個超負責任的人的話,你會發現這裡還有個小問題,就是刪掉舊文件和重命名臨時文件這兩個操作也不是原子性的...可能舊文件刪掉後臨時文件尚未改名系統便崩潰了,對於這種情況有3種辦法:
1.自驗證完整性:在保存的文件中加入能夠驗證完整性的內容,比如在結尾加入個特殊字符什麼的,如果在載入文件時發現有臨時文件存在的話,就驗證臨時文件是否完整,完整的話就使用臨時文件的內容,另外別忘了順便把之前未完成的工作完成了.
2.舊文件備份:這個的思路是在完成臨時文件的寫入後,不立刻刪掉舊文件,而是將舊文件改名為一個備份文件,然後將臨時文件改名為正常文件.當讀取文件時,如果正常文件不存在的話,就去讀備份文件.
3.原子性替換:大部分符合POSIX標準的操作系統都支持原子性文件替換,Windows號稱部分符合POSIX標準,遺憾的是這不是其中之一.不過Java7提供了跨平台的原子性文件替換方法,比如:
Files.move(tempFile.toPath(), oldFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
然而這個是Java7的功能,MC要求的最低版本是Java6,雖然我不覺得現在還有土鱉在使用Java6,但誰知道會不會真有人在用呢...而且這個方法有潛在的失敗可能,文檔中明確提到如果原子操作不可行(比如在Windows中進行跨盤複製時)的話會立刻拋出異常.
如果不考慮方法3的話,方法1和方法2的區別在於1是在發生問題時盡量使用新文件,2是在發生問題時盡量使用舊文件;1需要特別設計的文件格式,而2則不需要.
世界
世界的額外數據與持久化
WorldInfo類的setAdditionalProperties和getAdditionalProperty方法可以分配或獲取一個NBT節點,這些數據會隨着該WorldInfo所屬的World被一併保存或讀取.顯然在多人遊戲中這些數據不會被服務器自動同步給客戶端,如果你需要的話,可能得通過自定義封包來手動同步數據.
獲取WorldInfo的辦法是調用World類的getWorldInfo.
此外,GameRuler類也可以用來保存數據,然而它只能存儲字符串.
獲取一條線段上最近的磚塊
World類提供了rayTraceBlocks方法用於查找一條有向線段中第一個接觸線段的磚塊,它有如下3種重載(其中一個在1.7.10中叫func_147447_a):
rayTraceBlocks(Vec3 start, Vec3 end, boolean includeLiquidAndFire, boolean ignoreBlockWithoutBoundingBox, boolean returnLastUncollidableBlock)
start和end為線段的起點和終點;includeLiquidAndFire為是否包括火焰和液體;ignoreBlockWithoutBoundingBox為是否忽略無碰撞(也就是沒有碰撞箱)的磚塊;returnLastUncollidableBlock為是否一定要返回一個非null結果,如果此參數為true的話,即使未找到結果也會返回最後一個無碰撞磚塊的位置,同時返回結果(一個MovingObjectPosition實例)中的typeOfHit字段會被標記為MISS,如果為false,或者連無碰撞磚塊都找不到的話,則會返回null.
rayTraceBlocks(Vec3 start, Vec3 end, boolean includeLiquidAndFire)
重載上一個,ignoreBlockWithoutBoundingBox和returnLastUncollidableBlock都為false.
rayTraceBlocks(Vec3 start, Vec3 end)
重載上一個,includeLiquidAndFire為false.
生成粒子特效
生成粒子特效可以用World類的spawnParticle,它的7個參數分別是"粒子類型"、粒子的初始位置和初始速度矢量,可用的粒子類型詳見http://minecraft.gamepedia.com/Particles中的Particle name一項.
此外,只有在客戶端調用spawnParticle才會有效果,在服務器端調用會被靜默地忽視掉.
自定義粒子特效
如果現有的粒子特效不夠用的話,可以自行創建一個EntityFX的子類,然後在客戶端的代理器(Proxy)通過Minecraft.getMinecraft().effectRenderer.addEffect來添加粒子特效.
需要注意的有兩個問題,一個是添加自定義粒子特效必須在客戶端進行,因為服務器就壓根沒EffectRenderer和EntityFX這兩個東西...第二個是在添加時必須根據待添加的世界的isRemote方法判斷是否為客戶端世界,即使你的代碼已經是在客戶端代理器中也必須進行這個判斷,並且只在客戶端世界中添加特效,如果不這樣做的話,你會有一定幾率在單人遊戲時遇到一個奇特的競態條件,具體表現形式為在創建特效時遊戲會有一定幾率彈出,錯誤原因是隨機的一個實體的moveEntity方法在服務器線程中出現了一個NullPointerException,出現位置是在一個"for (int i = 0; i < list.size(); ++i)"的循環體中,其中list.size()為0...看起來像是服務器線程和客戶端線程在同時操作一個列表時發生了衝突.
這裡給出一個簡單的示例 - 砍怪時飆血.
首先是在客戶端代理器中監聽實體遭受攻擊事件,然後創建相應的粒子特效:
[code lang="java"]
@SubscribeEvent
public void onEntityAttacked(LivingAttackEvent event) {
Entity attacking = event.source.getSourceOfDamage(); //attacking為攻擊者
if(attacking == null || !attacking.worldObj.isRemote) //對於攻擊者不是實體,或者該世界不為客戶端世界的情況,直接返回
return;
EntityLivingBase attacked = event.entityLiving; //attacked為受害者
for(int i = 0; i < 10; i++)
{
Minecraft.getMinecraft().effectRenderer.addEffect( //添加一個待會製作的EntityBlood實體,它的參數是遊戲世界,3個double表示的粒子位置,以及1個Vec3表示的方向矢量
new EntityBlood(attacked.worldObj,
attacked.posX,
attacked.posY + attacked.getEyeHeight(),
attacked.posZ,
Vec3.createVectorHelper(attacking.posX - attacked.posX, //根據攻擊者和被攻擊者的位置計算出血噴出的方向
attacking.posY - attacked.posY - attacked.getEyeHeight(),
attacking.posZ - attacked.posZ).normalize())); //方向矢量最好是單位長度,因此做一次規格化
}
}
[/code]
然後要製作EntityBlood類,它的代碼並不複雜.
[code lang="java"]
@SideOnly(Side.CLIENT)
public class EntityBlood extends EntityFX {
private final float PI3 = (float)(Math.PI / 3.0);
private final float PI6 = (float)(Math.PI / 6.0);
public EntityBlood(World world, double posX, double posY, double posZ, Vec3 direction) {
super(world, posX, posY, posZ);
double speed = rand.nextDouble() * 0.3d; //設定速度為0.0~0.3範圍內的隨機數
direction.rotateAroundY((rand.nextFloat() - 0.5f) * PI3); //讓噴血方向帶有一定的隨機性
direction.rotateAroundX((rand.nextFloat() - 0.5f) * PI6);
direction.rotateAroundZ((rand.nextFloat() - 0.5f) * PI6);
setVelocity(direction.xCoord * speed, direction.yCoord * speed, direction.zCoord * speed); //根據方向矢量設定速度矢量
setSize(0.1f, 0.1f); //設定粒子的碰撞尺寸,如果不想要碰撞的話不妨試試noClip = true ?
this.particleMaxAge = (int)(10f / (this.rand.nextFloat() * 0.75f + 0.25f)); //設定粒子的壽命,單位為tick
this.particleGravity = 0.3f; //受重力加速度影響的係數,1.0為每tick Y方向速度減-0.04 (不考慮空氣阻力的話)
this.particleGreen = 0.0f; //RGB三顏色中的G係數,默認全為1.0,即白色/顯示紋理正常顏色
this.particleBlue = 0.0f; //將G和B全改為0的話,那就是紅色/紋理"偏"紅
this.particleAlpha = 1.0f; //不透明度,默認就是1.0...
this.particleScale = 3.0f; //尺寸係數,默認是1.0~2.0之間的隨機數
}
}
[/code]
關於粒子紋理圖可以見http://minecraft.gamepedia.com/File:Particles.png,默認的粒子紋理是左上角的那個小方點.想修改的話可以用setParticleTextureIndex來設定所使用的部分,計算方法是把圖片分成16x16共256份,然後按照從左往右,從上往下的順序數,第一行第一列(也就是默認部分)為0,第一行第二列為1,第二行第一列為16.憤怒的村民臉為83.
想讓血液紋理為默認的8種爆炸煙霧之一的話,只要在構造函數中添加:
setParticleTextureIndex(rand.nextInt(8));
如果想要自定義紋理的話倒也不是很麻煩,但效率很低,由於默認的EffectRenderer的限制,要在每次渲染時重啟Tessellator,然後重新綁定紋理,完成渲染後還要再次重啟Tessellator並綁定回原紋理.這裡給出一個效率不是很高的實現:
@SideOnly(Side.CLIENT) public class EntityBlood extends EntityFX { private static TextureManager textureManager = Minecraft.getMinecraft().renderEngine; //值得注意的是,這裡ResourceLocation必須寫完整地址,不能像Block或Item中那樣簡寫為"examplemod:splatt"了... private static ResourceLocation texture = new ResourceLocation("examplemod:textures/particles/splatt.png"); private final float PI3 = (float)(Math.PI / 3.0); private final float PI6 = (float)(Math.PI / 6.0); public EntityBlood(World world, double posX, double posY, double posZ, Vec3 direction) { super(world, posX, posY, posZ); double speed = rand.nextDouble() * 0.3d; //設定速度為0.0~0.3範圍內的隨機數 direction.rotateAroundY((rand.nextFloat() - 0.5f) * PI3); //讓噴血方向帶有一定的隨機性 direction.rotateAroundX((rand.nextFloat() - 0.5f) * PI6); direction.rotateAroundZ((rand.nextFloat() - 0.5f) * PI6); setVelocity(direction.xCoord * speed, direction.yCoord * speed, direction.zCoord * speed); //根據方向矢量設定速度矢量 setSize(0.1f, 0.1f); //設定粒子的碰撞尺寸,如果不想要碰撞的話不妨試試noClip = true ? this.particleMaxAge = (int)(10f / (this.rand.nextFloat() * 0.75f + 0.25f)); //設定粒子的壽命,單位為tick this.particleGravity = 0.3f; //受重力加速度影響的係數,1.0為每tick Y方向速度減-0.04 (不考慮空氣阻力的話) this.particleGreen = 0.0f; //RGB三顏色中的G係數,默認全為1.0,即白色/顯示紋理正常顏色 this.particleBlue = 0.0f; //將G和B全改為0的話,那就是紅色/紋理"偏"紅 this.particleAlpha = 1.0f; //不透明度,默認就是1.0... this.particleScale = 3.0f; //尺寸係數,默認是1.0~2.0之間的隨機數 } @Override public int getFXLayer() { //EffectRenderer根據使用的紋理圖不同將粒子分為了3類(層),0類使用粒子紋理圖,1類使用磚塊紋理圖,2類使用物品紋理圖. //這裡我們跟2類共用一層倒也沒什麼玄學,純粹是因為物品紋理圖(TextureMap.locationItemsTexture)是public,而粒子紋理圖是private... return 2; } @Override public void renderParticle(Tessellator tessellator, float delta, float rotationX, float rotationXZ, float rotationZ, float rotationYZ, float rotationXY) { tessellator.draw(); //Tessellator一次渲染只能使用一種紋理,因此要重啟並重新綁定 tessellator.startDrawingQuads(); textureManager.bindTexture(texture); float f6 = 0.0f; //這4個參數是用來控制UV的,如果你是一個粒子對應一整個紋理的話,那就保留這個設定就行了 float f7 = 1.0f; float f8 = 0.0f; float f9 = 1.0f; float scale = 0.1F * this.particleScale; float f11 = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)delta - interpPosX); float f12 = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)delta - interpPosY); float f13 = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)delta - interpPosZ); tessellator.setColorRGBA_F(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha); tessellator.addVertexWithUV((double)(f11 - rotationX * scale - rotationYZ * scale), (double)(f12 - rotationXZ * scale), (double)(f13 - rotationZ * scale - rotationXY * scale), (double)f7, (double)f9); tessellator.addVertexWithUV((double)(f11 - rotationX * scale + rotationYZ * scale), (double)(f12 + rotationXZ * scale), (double)(f13 - rotationZ * scale + rotationXY * scale), (double)f7, (double)f8); tessellator.addVertexWithUV((double)(f11 + rotationX * scale + rotationYZ * scale), (double)(f12 + rotationXZ * scale), (double)(f13 + rotationZ * scale + rotationXY * scale), (double)f6, (double)f8); tessellator.addVertexWithUV((double)(f11 + rotationX * scale - rotationYZ * scale), (double)(f12 - rotationXZ * scale), (double)(f13 + rotationZ * scale - rotationXY * scale), (double)f6, (double)f9); tessellator.draw(); tessellator.startDrawingQuads(); textureManager.bindTexture(TextureMap.locationItemsTexture); //綁定回物品紋理圖 } }
這裡的紋理我放在了"assets/examplemod/textures/particles"中,紋理我使用的是一張HL1的噴漆圖,希望G胖不會來爆我菊花吧 233333
Gabe:(此處應用古朗特的語氣)Too late! YOU ARE DEAD!
不管怎麼說,效果還是很好的,雖然說是效率有問題,但對於幾十甚至幾百個粒子來說,還是足以力保60fps大關的,如果你真的追求高性能自定義粒子渲染的話,可以不妨自己寫一個粒子渲染器,EffectRenderer的代碼並不複雜,RenderWorldLastEvent也可以作為自定義渲染器啟動渲染的觸發事件.
配方
刪除已有的合成與冶煉配方
所有的合成配方都儲存在CraftingManager的recipes字段中,你可以通過調用CraftingManager.getInstance().getRecipeList()來獲取它,它儲存有所有已註冊的合成配方,你可以遍歷這個列表然後找出指定的合成並刪掉它,這裡有一個現成的已封裝好的代碼,以及使用範例:
/** * 移除一個合成配方,此版本僅對比合成的輸出物,而且輸出物的類型,數量和itemDamage(如果配方指定了itemDamage的話)必須全部匹配才會執行刪除操作. * @param output 輸出物的物品棧 * @return 是否找到並移除了一個配方 */ public boolean removeRecipe(ItemStack output) { for(Iterator<Object> iterator = CraftingManager.getInstance().getRecipeList().iterator(); iterator.hasNext();) { IRecipe recipe = (IRecipe)iterator.next(); ItemStack recipeOutput = recipe.getRecipeOutput(); if(OreDictionary.itemMatches(recipeOutput, output, false) && recipeOutput.stackSize == output.stackSize) { iterator.remove(); return true; } } return false; } /** * 移除一個合成配方,此版本會詳細對比合成的輸入物,並且考慮輸入物的類型和itemDamage(如果配方指定了itemDamage的話). * @param input 見removeRecipe(ItemStack[] input, ItemStack output) * @return 是否找到並移除了一個配方 */ public boolean removeRecipe(ItemStack[] input) { return removeRecipe(input, null); } /** * 移除一個合成配方,此版本會詳細對比合成的輸入物,並且考慮輸入物的類型和itemDamage(如果配方指定了itemDamage的話). * 此外,還可以指定一個輸出物用於篩選配方提高效率,輸出物只考慮類型,不考慮數量和itemDamage. * @param input 輸入物組成的合成公式,需要嚴格按照添加合成配方時的樣式進行,比如火炬是長度為2的數組,裡面是1個煤和1個木棍; * 工具台是長度為4的數組,裡面是4個木頭;告示牌是長度為9的數組,裡面是6個木頭,1個null,1個木棍,然後再跟1個null. * @param output 輸出物,用於預剔除來提高效率,可選參數,可以是null - 不進行預剔除. * @return 是否找到並移除了一個配方 */ public boolean removeRecipe(ItemStack[] input, ItemStack output) { dirtygoto: for(Iterator<Object> iterator = CraftingManager.getInstance().getRecipeList().iterator(); iterator.hasNext();) { IRecipe recipe = (IRecipe)iterator.next(); ItemStack recipeOutput = recipe.getRecipeOutput(); if(recipeOutput == null || (output != null && recipeOutput.getItem() != output.getItem())) continue; Object[] targetStacks; if(recipe instanceof ShapedRecipes) targetStacks = ((ShapedRecipes)recipe).recipeItems; else if(recipe instanceof ShapedOreRecipe) targetStacks = ((ShapedOreRecipe)recipe).getInput(); else if(recipe instanceof ShapelessRecipes) targetStacks = ((ShapelessRecipes)recipe).recipeItems.toArray(); else if(recipe instanceof ShapelessOreRecipe) targetStacks = ((ShapelessOreRecipe)recipe).getInput().toArray(); else continue; if(targetStacks.length == input.length) { for(int i = 0; i < targetStacks.length; i++) { if(targetStacks[i] instanceof ItemStack) { if(!OreDictionary.itemMatches((ItemStack)targetStacks[i], input[i], false)) continue dirtygoto; } else if(targetStacks[i] instanceof ArrayList) { boolean match = false; ArrayList list = (ArrayList)targetStacks[i]; for(Object object : list) { if(OreDictionary.itemMatches((ItemStack)object, input[i], false)) { match = true; break; } } if(!match) continue dirtygoto; } } iterator.remove(); return true; } } return false; }
@EventHandler public void postInit(FMLPostInitializationEvent event) { //刪除工具台 ItemStack itemStack = new ItemStack(Blocks.crafting_table); removeRecipe(itemStack); //刪除告示牌 ItemStack plank = new ItemStack(Blocks.planks); ItemStack stick = new ItemStack(Items.stick); ItemStack sign = new ItemStack(Items.sign); ItemStack[] itemStacks = new ItemStack[] { plank, plank, plank, plank, plank, plank, null, stick, null }; removeRecipe(itemStacks, sign); //刪除末地眼 ItemStack enderPearl = new ItemStack(Items.ender_pearl); ItemStack powerDaze = new ItemStack(Items.blaze_powder); ItemStack enderEye = new ItemStack(Items.ender_eye); itemStacks = new ItemStack[] {enderPearl, powerDaze}; removeRecipe(itemStacks, enderEye); //刪除火炬 - 火炬有2種合成,因此要刪兩遍 ItemStack torch = new ItemStack(Blocks.torch); itemStacks = new ItemStack[] {new ItemStack(Items.coal, 1, 0), stick}; //使用煤炭的合成 removeRecipe(itemStacks, torch); itemStacks = new ItemStack[] {new ItemStack(Items.coal, 1, 1), stick}; //使用焦煤的合成 removeRecipe(itemStacks, torch); }
GUI
在GUI界面中渲染玩家
如果是在遊戲中的話這倒不是難事,GuiInventory類的drawEntityOnScreen就可以繪製一個生物實體到屏幕上.但如果不在遊戲中,比如在主菜單時,想繪製一個玩家的話就不容易了,因為這時沒有遊戲世界,也沒有玩家屍體.但也並非不可實現.這裡給出一個參考示例,一個開箱即用的玩家渲染器.
16-1-25更新:修復了導致遊戲內玩家模型渲染錯誤的bug...兩個版本的變更代碼均為static塊內的內容.
1.7.10版
/** * 在屏幕中以正交投影渲染一個玩家,建議在DrawScreenEvent.Post事件或界面類的drawScreen中進行. */ @SideOnly(Side.CLIENT) public class ScreenPlayerRenderer { //玩家模型 private static final ModelBiped steveModel; public static final ResourceLocation defaultSteveTexture = new ResourceLocation("textures/entity/steve.png"); private static final Map<String, ResourceLocation> skinCache = new ConcurrentHashMap<String, ResourceLocation>(); private ModelBiped model = steveModel; private boolean isAlex = false; private float headPitch = 0f, headYaw = 0f; private float frame = 0f, walk = 0f;; static { steveModel = new ModelBiped(); } /** * 設置皮膚貼圖為默認貼圖 */ public void setSkin() { Minecraft.getMinecraft().renderEngine.bindTexture(defaultSteveTexture); } /** * 根據玩家名稱自動從網上獲取貼圖,在貼圖讀取完畢前,玩家貼圖會顯示為默認貼圖. */ public void setSkin(String name) { ResourceLocation resourceLocation = skinCache.get(name); if(resourceLocation == null) { //此處為net.minecraft.util下的StringUtils resourceLocation = new ResourceLocation("skins/" + StringUtils.stripControlCodes(name)); AbstractClientPlayer.getDownloadImageSkin(resourceLocation, name); skinCache.put(name, resourceLocation); } Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation); } /** * 設置皮膚貼圖為一個已有的貼圖 */ public void setSkin(ResourceLocation resourceLocation) { Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation); } /** * 設置頭部的朝向,默認是平視正前方 * @param pitch 負數朝上看,正數朝下看,單位為角度制 * @param yaw 負數朝左看,正數朝右看,單位為角度制 */ public void setHeadLook(float pitch, float yaw) { headPitch = pitch; headYaw = yaw; } /** * 手動設定當前幀數,通常來說是不需要的 */ public void setFrame(float frameTime) { frame = frameTime; } /** * 設定人物行走動作,0.0為原地站立不動,1.0為正常行走 * @param speed */ public void setWalk(float speed) { walk = speed; } public void render(int posX, int posY, float pitch, float yaw, float roll, float size) { render(posX, posY, pitch, yaw, roll, size, 0f); } /** * 在屏幕上渲染玩家. * @param posX 屏幕坐標上的原點位置的X坐標 * @param posY 屏幕坐標上的原點位置的Y坐標 * @param pitch 繞X軸旋轉角度,單位為角度制.負數上仰,正數下俯.注意坐標原點是在玩家的腳下而不是正中心. * @param yaw 繞Y軸旋轉的角度,單位為角度制.負數向左,正數向右. * @param roll 繞Z軸旋轉的角度,單位為角度制 * @param size 縮放倍數,0.0的話就看不見了...建議在2.0~4.0之間. * @param delta 遞增幀數的參數,用於維持播放玩家的行走動作和手臂自然擺動等,通過控制數值大小可以控制動作速度. */ public void render(int posX, int posY, float pitch, float yaw, float roll, float size, float delta) { frame += delta; model.setRotationAngles(frame * walk, walk, frame, headYaw, headPitch, 0f, null); GL11.glEnable(GL11.GL_COLOR_MATERIAL); GL11.glPushMatrix(); //這裡是個很玄學的地方,需要把深度測試的方式顛倒一下才能正常渲染,我承認我也不知道這為什麼...但它JustWork™! //不這樣做的話,渲染出來的效果會跟沒開深度測試似的 GL11.glEnable(GL11.GL_DEPTH_TEST); GL11.glDepthFunc(GL11.GL_GREATER); GL11.glTranslatef(posX, posY, -500.0f); GL11.glScalef(size, size, size); GL11.glRotatef(roll, 0.0f, 0.0f, 1.0f); GL11.glRotatef(yaw, 0.0f, 1.0f, 0.0f); GL11.glRotatef(pitch, 1.0f, 0.0f, 0.0f); GL11.glColor3f(1.0f, 1.0f, 1.0f); model.bipedHead.render(1.0f); model.bipedBody.render(1.0f); model.bipedRightArm.render(1.0f); model.bipedLeftArm.render(1.0f); model.bipedRightLeg.render(1.0f); model.bipedLeftLeg.render(1.0f); model.bipedHeadwear.render(1.0f); GL11.glDepthFunc(GL11.GL_LEQUAL);; GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glPopMatrix(); GL11.glDisable(GL11.GL_COLOR_MATERIAL); } }
1.8版
1.8版有個小小的缺陷,就是無法自動判斷玩家使用的皮膚貼圖類型是Steve還是Alex.
/** * 在屏幕中以正交投影渲染一個玩家,建議在DrawScreenEvent.Post事件或界面類的drawScreen中進行. */ @SideOnly(Side.CLIENT) public class ScreenPlayerRenderer { //Steve和Alex的模型 private static final ModelBiped steveModel, alexModel; public static final ResourceLocation defaultSteveTexture = new ResourceLocation("textures/entity/steve.png"), defaultAlexTexture = new ResourceLocation("textures/entity/alex.png"); private static final Map<String, ResourceLocation> skinCache = new ConcurrentHashMap<String, ResourceLocation>(); private ModelBiped model = steveModel; private boolean isAlex = false; private float headPitch = 0f, headYaw = 0f; private float frame = 0f, walk = 0f;; static { steveModel = new ModelBiped(); alexModel = new ModelBiped(); copyModel(new ModelPlayer(0f, false), steveModel); copyModel(new ModelPlayer(0f, true), alexModel); } /** * 1.8中玩家模型是ModelPlayer類,它有個缺點是setRotationAngles的實體參數必須為非null * (ModelBiped類的setRotationAngles的實體參數就可以是null) 故此這裡創建一個替代品並把必要的參數複製過去 */ private static void copyModel(ModelPlayer src, ModelBiped dest) { dest.bipedBody = src.bipedBody; dest.bipedHead = src.bipedHead; dest.bipedHeadwear = src.bipedHeadwear; dest.bipedLeftArm = src.bipedLeftArm; dest.bipedRightArm = src.bipedRightArm; dest.bipedLeftLeg = src.bipedLeftLeg; dest.bipedRightLeg = src.bipedRightLeg; } /** * 設置皮膚類型是否為Alex */ public void setAlex(boolean isAlex) { this.isAlex = isAlex; model = isAlex ? alexModel : steveModel; } /** * 設置皮膚貼圖為默認貼圖 */ public void setSkin() { Minecraft.getMinecraft().renderEngine.bindTexture(isAlex ? defaultAlexTexture : defaultSteveTexture); } /** * 根據玩家名稱自動從網上獲取貼圖,在貼圖讀取完畢前,玩家貼圖會顯示為默認貼圖. */ public void setSkin(String name) { ResourceLocation resourceLocation = skinCache.get(name); if(resourceLocation == null) { //此處為net.minecraft.util下的StringUtils resourceLocation = new ResourceLocation("skins/" + StringUtils.stripControlCodes(name)); AbstractClientPlayer.getDownloadImageSkin(resourceLocation, name); skinCache.put(name, resourceLocation); } Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation); } /** * 設置皮膚貼圖為一個已有的貼圖 */ public void setSkin(ResourceLocation resourceLocation) { Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation); } /** * 設置頭部的朝向,默認是平視正前方 * @param pitch 負數朝上看,正數朝下看,單位為角度制 * @param yaw 負數朝左看,正數朝右看,單位為角度制 */ public void setHeadLook(float pitch, float yaw) { headPitch = pitch; headYaw = yaw; } /** * 手動設定當前幀數,通常來說是不需要的 */ public void setFrame(float frameTime) { frame = frameTime; } /** * 設定人物行走動作,0.0為原地站立不動,1.0為正常行走 * @param speed */ public void setWalk(float speed) { walk = speed; } public void render(int posX, int posY, float pitch, float yaw, float roll, float size) { render(posX, posY, pitch, yaw, roll, size, 0f); } /** * 在屏幕上渲染玩家. * @param posX 屏幕坐標上的原點位置的X坐標 * @param posY 屏幕坐標上的原點位置的Y坐標 * @param pitch 繞X軸旋轉角度,單位為角度制.負數上仰,正數下俯.注意坐標原點是在玩家的腳下而不是正中心. * @param yaw 繞Y軸旋轉的角度,單位為角度制.負數向左,正數向右. * @param roll 繞Z軸旋轉的角度,單位為角度制 * @param size 縮放倍數,0.0的話就看不見了...建議在2.0~4.0之間. * @param delta 遞增幀數的參數,用於維持播放玩家的行走動作和手臂自然擺動等,通過控制數值大小可以控制動作速度. */ public void render(int posX, int posY, float pitch, float yaw, float roll, float size, float delta) { frame += delta; model.setRotationAngles(frame * walk, walk, frame, headYaw, headPitch, 0f, null); GlStateManager.enableColorMaterial(); GlStateManager.pushMatrix(); //這裡是個很玄學的地方,需要把深度測試的方式顛倒一下才能正常渲染,我承認我也不知道這為什麼...但它JustWork™! //不這樣做的話,渲染出來的效果會跟沒開深度測試似的 GlStateManager.depthFunc(GL11.GL_GREATER); GlStateManager.translate(posX, posY, -500.0f); //正常情況下深度應該是500,但由於這裡的深度測試被顛倒了,因此要取-500才會渲染出來 GlStateManager.scale(size, size, size); GlStateManager.rotate(roll, 0.0f, 0.0f, 1.0f); GlStateManager.rotate(yaw, 0.0f, 1.0f, 0.0f); GlStateManager.rotate(pitch, 1.0f, 0.0f, 0.0f); GlStateManager.color(1.0f, 1.0f, 1.0f); model.bipedHead.render(1.0f); model.bipedBody.render(1.0f); model.bipedRightArm.render(1.0f); model.bipedLeftArm.render(1.0f); model.bipedRightLeg.render(1.0f); model.bipedLeftLeg.render(1.0f); model.bipedHeadwear.render(1.0f); GlStateManager.depthFunc(GL11.GL_LEQUAL); GlStateManager.popMatrix(); GlStateManager.disableColorMaterial(); } }
使用方法,比如在主菜單渲染玩家:
private ScreenPlayerRenderer spr; @EventHandler public void init(FMLPostInitializationEvent event) { spr = new ScreenPlayerRenderer(); } //別忘了在MinecraftForge.EVENT_BUS中註冊事件句柄 @SubscribeEvent public void onGuiRender(DrawScreenEvent.Post event) { if(event.gui instanceof GuiMainMenu) { spr.setSkin("szszss"); spr.setWalk(1.0f); spr.setHeadLook(-10f, 30f); spr.render(55, 55, 10f, -30f, 0.0f, 4.0f, event.renderPartialTicks); } }
事件總線
註冊Forge/FML事件總線時出錯
在Forge事件總線(EVENT_BUS、TERRAIN_GEN_BUS和ORE_GEN_BUS)和FML事件總線中註冊事件總線時如果遇到了IllegalArgumentException異常的話,很可能是類中包含了帶有@SubscribeEvent註解,但卻有非法參數的方法.事件監聽方法要求必須有並且只能有一個類型為Event或其派生類的參數.
註冊事件無效,監聽方法不被調用
注意監聽方法的訪問級,必須是非靜態的公共方法,否則會被Forge默默地跳過...
此外還有其他因素,如果你的Mod在與其他Mod協同工作的話,事件是否被更高優先級的監聽器攔截並取消?聲明@SubscribeEvent註解時將receiveCanceled改為true可以接受被取消的事件;是否註冊了恰當的事件總線?
總共有多少個事件總線
FML/Forge中總共有5條事件總線.Forge有3個通過FML的EventBus實現的,分別是處理所有的實體事件和絕大部分遊戲事件的EVENT_BUS,處理地形生成的TERRAIN_GEN_BUS和處理礦物生成的ORE_GEN_BUS. FML有2個,分別是用自己的EventBus實現的(通過FMLCommonHandler.INSTANCE.bus()來獲取)和用Guava的EventBus實現的(Coremod所使用的ModContainer的registerBus方法接受的那個參數即為該總線).
為什麼有兩套事件總線(FML的和Guava的)
大概是因為Guava事件總線處理的事件都是遊戲初始化時的事件,此時FML尚未初始化完畢,總線系統尚不可用,因此就暫用Guava的.
Guava和FML事件總線的最大區別在於Guava事件總線是用反射來調用監聽方法,FML事件總線是通過實時字節碼生成來即時生成一個調用器,以直接調用的方式來調用監聽方法,效率比反射高到不知道哪去了(順便悄悄安利一下,我做的AsmEventBus也是通過字節碼生成的高效事件總線,當時以為是epic work,後來發現FML早在我之前就實現了...fucking life).
此外,FML的事件總線使用@SubscribeEvent來標記訂閱者,普通Mod的主類用@EventHandler來標記初始化事件訂閱者,而Coremod的事件處理類用@Subscribe來處理初始化事件訂閱者.
為什麼Forge和FML有獨立的事件總線
客觀原因:FML是一個取代ModLoader,負責實現Mod的加載和管理,以及必要的接口的工具,而Forge從最初到現在一直只是個功能庫,雖然Forge出現早於FML,但在層次上現在的Forge是基於FML的,這註定它無法處理一些底層事物,比如玩家的輸入以及服務器狀態改變等.
主觀原因:Forge和FML是兩個聯姻的不同項目,別忘了直到1.8的時候cpw宣布"Fuck, I quit"之前,FML一直是在一個獨立的包下.
真要嚴格區分兩者幾乎是不可能的,如果只是粗略總結的話,Forge的事件總線用來處理頂層邏輯(比如實體之類的)以及GUI.FML的事件總線用來處理底層操作,比如玩家的鼠標鍵盤輸入,服務器中玩家的登入登出.但由於各種歷史原因,兩者之間互有交集,比如Forge也會處理鼠標事件,而FML會處理冶煉和合成事件.
FML的事件總線能處理哪些事件
見cpw.mods.fml.common.gameevent和cpw.mods.fml.client.event包下的類.
好吧我知道如果我不寫出來的話有些人是永遠不會去看的...這裡大概寫一下能處理什麼功能.
- 玩家的鼠標與鍵盤輸入操作.(FML將它歸位了Common,不過這難道不應該是ClientOnly嗎?算了不管它了...)
- 玩家的撿起物品,合成物品,取出冶煉物品,登錄服務器,登出服務器,復活,(字面含義上)前往另一個世界
- 每幀(Tick)觸發的事件,可以是ServerTickEvent(服務器每幀開始和結束時觸發),ClientTickEvent(客戶端每幀開始和結束時觸發),WorldTickEvent(WorldServer的tick開始與結束時觸發),PlayerTickEvent(EntityPlayer的onUpdate開始與結束觸發)和RenderTickEvent(EntityRenderer的updateCameraAndRender開始與結束時觸發)
- 客戶端獨有,ConfigChangedEvent改變Mod配置時觸發
FML的Guava事件總線能處理哪些事件
見cpw.mods.fml.common.event包下的類.基本上所有在Mod主類中能處理的那些事件都是由Guava事件總線來實現的.
Coremod
如何查詢全混淆名和半混淆名
問這個問題的人按說應該讀過Coremod教程,那他是怎麼才會沒看到教程中的AsmShooterMappingData呢...不管怎麼說,這裡再貼一遍.
ASMShooter全稱Aya Syameimaru's Miniskirt Shooter,簡稱ASMShooter,是本文作者開發的一個黑項目,現已由於過於黑,而被SCP基金會勒令停止開發並無限期凍結.說白了,這項目至今沒出成果,而且已經停止開發了......
不過別急着關窗口,它的一個子項目已經開發完成了,我們可以利用它的產品:ASMShooterMappingData,進行查表定位.
ASMShooterMappingData是一個xml文件,它構建了一個完整的未混淆-半混淆-全混淆名的映射關係表,並且還詳細記述了每一個方法的參數和返回值,非常便於查詢.
更贊的是,在v2版協議中,它還增加了方法的描述符(由於FML內置的反混淆器存在,方法描述符在開發環境和遊戲環境下都是通用的)和類的全名,更方便開發者快速查詢信息.
ASMShooterMappingData在文件頭的MinecraftVersion標識了它對應的Minecraft版本,ProtocolVersion標識了格式版本.當前的格式(Protocol 3)為:
XML頭結點
--包結點(以Package命名 屬性Name為包名)
--|--類節點(以Class命名 屬性Unobscured為未混淆名,屬性Obscured為混淆名,屬性FullName為完整的未混淆類名)
--|--|--字段節點(以Field命名 屬性Unobscured為未混淆名,屬性Searge為半混淆名,屬性Obscured為混淆名)
--|--|--方法節點(以Method命名 屬性Unobscured為未混淆名,屬性Searge為半混淆名,屬性Obscured為混淆名)
--|--|--|--參數結點(以Param命名 內容文字為參數類型的未混淆名)
--|--|--|--返回值結點(以Return命名 內容文字為返回值類型的未混淆名)
--|--|--|--描述符結點(以Desc命名 內容文字為方法的未混淆描述符)
下載地址:
1.6.2,1.6.4,1.7.2,1.7.10:http://1drv.ms/1DDv2j2
棒!看起來能解決很多問題...
原來這麼久沒動靜是在憋大招。是在下輸了。
...
請問下如何畫物品欄里左邊那人物
翻了很久就是不會用..
好吧我翻了下耐久度顯示中的在開始頁顯示人物
太麻煩了!
創建假世界都出來了..........
這問題還是不問了
前一段時間忙着考試去了...其實如果只是簡單地渲染個玩家還是有辦法,發車了! ? http://blog.hakugyokurou.net/?p=1298#renderplayerongui
在這裡說幾個微小的問題(2016-1-9):
{
使用FML的EventBus實現的事件總線有五個而不是四個,還有一個是FMLEventChannel.eventBus,只不過這個被封裝起來了,FMLEventChannel這個類自己弄了幾個register、unregister、fireUserEvent方法包裝了一下。
附加數據部分,Forge弄了個IExtendedEntityProperties接口,不知道博主有沒有注意到QwQ。
1.8的多材質方塊直接寫blockstate下的json就行了,這也是為什麼材質包從1.8開始一下子變得高大上多了,因為可以自定義模型了(這也苦了那些Moder了2333)。
關於FML總線能處理哪些事件。。。本人正在整理,然而現在進展相當緩慢只到了字母F(然而處理了EntityEvent已經它的眾多子類已經快累shi我了QwQ)博主可以先看看2333。
另外據說1.8新建一個流體和1710及之前不太一樣QwQ哪天我整理一下。
}
驚了,原來EventBus還有這個福利,謝老司機指路 ? ?
當時寫BlockState時累趴了,就沒寫Forge擴展的部分,簡單看了一下,主要是提供了可存儲非連續非枚舉類型(比如浮點數)的屬性,可供渲染使用,但當然不能用於保存 233
那個坑加油填啊 ? 當初我只是想了下就放棄了 233333
這個多邊形會出現裂縫啊..
http://i12.tietuku.com/eebcdcd27709bcb9.png
如何解決..
奇怪的是我用集顯又沒了?
這個問題看着謎啊 😓 這是某個mod的?
自製的sao血條...
不過看來貌似沒有什麼解決方法就扔一邊了..
渣渣電腦,
今天在對Forge1.8 setupDecompWorkspace時
在decompileMc這一步出了java heap space錯誤
癥狀是到這一步電腦卡的飛起 通過任務管理器發現java.exe內存佔用只有300MB多
於是懷疑是gradlew.bat最大內存分配的不夠
找了半天沒找到設置java參數的地方。
最後終於在編輯gradlew.bat發現了set DEFAULT_JVM_OPTS=
於是後面加上-Xmx1024m 就沒啥問題了。
根據上面的注釋發現貌似可以在環境變量中直接用JAVA_OPTS設置,當初研究java的時候竟然沒發現。。。
不知道這個經歷有沒有用。。
這個確實有過,當時測試FGOW的時候在一個32位XP虛擬機中測試時在decompileMc那一步內存不足了,當時覺得用小內存的32位系統的應該太少了,所以沒怎麼當回事...
求教開發完成的mod怎麼混淆成可發布的mod?一直手動很苦惱
咦 gradlew build不行嗎...我記得那個構建完後就自動混淆了啊
驚哭,我好像明白了,一直被我忽略了的東西。感激不盡!
一直以為是開發用的客戶端是反混淆過的,現在想起覺得天真!
好像又說錯了,請無視
其實對於安裝了FML/Forge後的MC究竟混不混淆我也不是拿的很准 (手動斜眼) 可以肯定的是通過setupDecompWorkspace配置完的工作環境肯定是反混淆(也就是全都是"正常名字")的,沒有安裝FML/Forge的遊戲環境肯定是混淆的(也就是各種一個字兩個字的英文字母),但是對於安裝了FML/Forge的遊戲環境來說,它大概是半混淆(應該叫SeargeName,以原MCP組領導者Searge命名),也就是類名是正常名字,方法名字段名全是形如func_xxxx,field_xxxx的存在.嘛不管如何gradlew build出來的東西肯定是能正常用的 ?
一直以為是開發用的客戶端是反混淆過的,現在想起覺得天真……
請問,在gradlew下runclient有聲音,但是在intellij下run的話沒有聲音,該如何解決呢
我指的是通過idea進行測試的時候,遊戲客戶端會沒有聲音
為什麼打開Eclipse後提示
“缺少“mdk”的項目描述文件(.project)。此文件包含有關項目的重要信息。項目將在復原此文件之後才能正常工作。”
(gradlew eclipse執行時的日誌)
Starting Build
Settings evaluated using settings file 'D:\master\settings.gradle'.
Projects loaded. Root project using build file 'D:\Programming\JDK TASKS\mdk1.9\build.gradle'.
Included projects: [root project 'mdk1.9']
Evaluating root project 'mdk1.9' using build file 'D:\Programming\JDK TASKS\mdk1.9\build.gradle'.
#################################################
ForgeGradle 2.1-SNAPSHOT-da90449
https://github.com/MinecraftForge/ForgeGradle
#################################################
Powered by MCP unknown
http://modcoderpack.com
by: Searge, ProfMobius, Fesh0r,
R4wk, ZeuX, IngisKahn, bspkrs
#################################################
file or directory 'D:\Programming\JDK TASKS\mdk1.9\src\api\resources', not found
All projects evaluated.
Selected primary task 'eclipse' from project :
Tasks to be executed: [task ':deobfCompileDummyTask', task ':deobfProvidedDummyTask', task ':eclipseClasspath', task ':eclipseJdt', task ':eclipseProject', task ':getVersionJson', task ':extractNatives', task ':extractUserdev', task ':getAssetIndex', task ':getAssets', task ':makeStart', task ':makeEclipseCleanRunClient', task ':makeEclipseCleanRunServer', task ':eclipse']
:deobfCompileDummyTask (Thread[main,5,main]) started.
:deobfCompileDummyTask
Executing task ':deobfCompileDummyTask' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.
:deobfCompileDummyTask (Thread[main,5,main]) completed. Took 0.015 secs.
:deobfProvidedDummyTask (Thread[main,5,main]) started.
:deobfProvidedDummyTask
Executing task ':deobfProvidedDummyTask' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.
:deobfProvidedDummyTask (Thread[main,5,main]) completed. Took 0.0 secs.
:eclipseClasspath (Thread[main,5,main]) started.
:eclipseClasspath
Executing task ':eclipseClasspath' (up-to-date check took 0.016 secs) due to:
Task.upToDateWhen is false.
Failed to get resource: GET. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/net/minecraftforge/forgeBin/1.9-12.16.0.1862-1.9/forgeBin-1.9-12.16.0.1862-1.9.pom%5D
Failed to get resource: GET. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/net/minecraftforge/forgeBin/1.9-12.16.0.1862-1.9/forgeBin-1.9-12.16.0.1862-1.9.pom%5D
Failed to get resource: HEAD. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/com/mojang/realms/1.8.7/realms-1.8.7-sources.jar%5D
Failed to get resource: HEAD. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/lzma/lzma/0.0.1/lzma-0.0.1-sources.jar%5D
Failed to get resource: HEAD. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-sources.jar%5D
file or directory 'D:\Programming\JDK TASKS\mdk1.9\libs', not found
:eclipseClasspath (Thread[main,5,main]) completed. Took 5.382 secs.
:eclipseJdt (Thread[main,5,main]) started.
:eclipseJdt
Executing task ':eclipseJdt' (up-to-date check took 0.0 secs) due to:
Task.upToDateWhen is false.
:eclipseJdt (Thread[main,5,main]) completed. Took 0.063 secs.
:eclipseProject (Thread[main,5,main]) started.
:eclipseProject
Executing task ':eclipseProject' (up-to-date check took 0.0 secs) due to:
Task.upToDateWhen is false.
:eclipseProject (Thread[main,5,main]) completed. Took 0.062 secs.
:getVersionJson (Thread[main,5,main]) started.
:getVersionJson
Executing task ':getVersionJson' (up-to-date check took 0.0 secs) due to:
Task.upToDateWhen is false.
:getVersionJson (Thread[main,5,main]) completed. Took 0.874 secs.
:extractNatives (Thread[main,5,main]) started.
:extractNatives
Skipping task ':extractNatives' as task onlyIf is false.
:extractNatives SKIPPED
:extractNatives (Thread[main,5,main]) completed. Took 0.468 secs.
:extractUserdev (Thread[main,5,main]) started.
:extractUserdev
Skipping task ':extractUserdev' as it is up-to-date (took 0.031 secs).
:extractUserdev UP-TO-DATE
:extractUserdev (Thread[main,5,main]) completed. Took 0.031 secs.
:getAssetIndex (Thread[main,5,main]) started.
:getAssetIndex
Executing task ':getAssetIndex' (up-to-date check took 0.0 secs) due to:
Task.upToDateWhen is false.
:getAssetIndex UP-TO-DATE
:getAssetIndex (Thread[main,5,main]) completed. Took 0.218 secs.
:getAssets (Thread[main,5,main]) started.
:getAssets
Executing task ':getAssets' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.
Current status: 47/1045 4%
Current status: 89/1045 8%
Current status: 133/1045 12%
Current status: 171/1045 16%
Current status: 230/1045 22%
Current status: 258/1045 24%
Current status: 277/1045 26%
Current status: 338/1045 32%
Current status: 388/1045 37%
Current status: 445/1045 42%
Current status: 489/1045 46%
Current status: 508/1045 48%
Current status: 530/1045 50%
Current status: 578/1045 55%
Current status: 636/1045 60%
Current status: 698/1045 66%
Current status: 750/1045 71%
Current status: 792/1045 75%
Current status: 839/1045 80%
Current status: 876/1045 83%
Current status: 924/1045 88%
Current status: 972/1045 93%
Current status: 1029/1045 98%
:getAssets (Thread[main,5,main]) completed. Took 23.822 secs.
:makeStart (Thread[main,5,main]) started.
:makeStart
Corrupted Cache!
Checksums found: 70d0e3c7f5bf7ad90270fe7578a9e8cf
85e93da6a7d7dc267d3204b930266017
b75fa7ddad441b67cdb73385a196ad92
e5996ed74152cbbb7312d749cf221c92
cf8a30818ddc58d0f0c6a0859382e8aa
22e773181235177142a5202751eaac1e
c5f6597e451b0cd2a2dc70143f385b7d
b5c2e82d57b428121f9679b456ab5487
cdee673be8f34ed15d188154cf6566d1
9b88db4333afdcd5697183dd5b216ed2
f0f787006da66d122e52ae7863da990c
15ae89f5a277e99b67de8e64eab5b855
cff0b4d54e15ffd3c03cac5c0def2e35
266566a138ea5c3ea60cdb62fba3ac4e
2e238f662134517b5914cb22ccdb7ae3
4513835928e5ad3e43cb32959a46485c
Checksums calculated: 70d0e3c7f5bf7ad90270fe7578a9e8cf
85e93da6a7d7dc267d3204b930266017
b75fa7ddad441b67cdb73385a196ad92
e5996ed74152cbbb7312d749cf221c92
cf8a30818ddc58d0f0c6a0859382e8aa
22e773181235177142a5202751eaac1e
c5f6597e451b0cd2a2dc70143f385b7d
b5c2e82d57b428121f9679b456ab5487
cdee673be8f34ed15d188154cf6566d1
9b88db4333afdcd5697183dd5b216ed2
f0f787006da66d122e52ae7863da990c
15ae89f5a277e99b67de8e64eab5b855
cff0b4d54e15ffd3c03cac5c0def2e35
266566a138ea5c3ea60cdb62fba3ac4e
f5608260fcc54059e8eaf48b61e90597
4513835928e5ad3e43cb32959a46485c
Executing task ':makeStart' (up-to-date check took 0.0 secs) due to:
Output file C:\Users\Administrator\.gradle\caches\minecraft\net\minecraftforge\forge\1.9-12.16.0.1862-1.9\start\.cache has been removed.
[ant:javac] Compiling 6 source files to C:\Users\Administrator\.gradle\caches\minecraft\net\minecraftforge\forge\1.9-12.16.0.1862-1.9\start
[ant:javac] 警告: [options] 未與 -source 1.6 一起設置引導類路徑
[ant:javac] 1 個警告
:makeStart (Thread[main,5,main]) completed. Took 1.653 secs.
:makeEclipseCleanRunClient (Thread[main,5,main]) started.
:makeEclipseCleanRunClient
Skipping task ':makeEclipseCleanRunClient' as it is up-to-date (took 0.0 secs).
:makeEclipseCleanRunClient UP-TO-DATE
:makeEclipseCleanRunClient (Thread[main,5,main]) completed. Took 0.0 secs.
:makeEclipseCleanRunServer (Thread[main,5,main]) started.
:makeEclipseCleanRunServer
Skipping task ':makeEclipseCleanRunServer' as it is up-to-date (took 0.0 secs).
:makeEclipseCleanRunServer UP-TO-DATE
:makeEclipseCleanRunServer (Thread[main,5,main]) completed. Took 0.0 secs.
:eclipse (Thread[main,5,main]) started.
:eclipse
Executing task ':eclipse' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.
:eclipse (Thread[main,5,main]) completed. Took 0.016 secs.
BUILD SUCCESSFUL
Total time: 55.131 secs
Stopped 0 compiler daemon(s).
[是不是這幾行出了問題:]
Failed to get resource: GET. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/net/minecraftforge/forgeBin/1.9-12.16.0.1862-1.9/forgeBin-1.9-12.16.0.1862-1.9.pom%5D
Failed to get resource: GET. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/net/minecraftforge/forgeBin/1.9-12.16.0.1862-1.9/forgeBin-1.9-12.16.0.1862-1.9.pom%5D
Failed to get resource: HEAD. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/com/mojang/realms/1.8.7/realms-1.8.7-sources.jar%5D
Failed to get resource: HEAD. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/lzma/lzma/0.0.1/lzma-0.0.1-sources.jar%5D
Failed to get resource: HEAD. [HTTP HTTP/1.1 403 Forbidden: https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-sources.jar%5D
file or directory 'D:\Programming\JDK TASKS\mdk1.9\libs', not found
這個...要不試試手動導入吧 ? MDK目錄下沒有.project文件嗎?
請問一下,drawEntityOnScreen在遊戲中的時候也需要獲得EntityLiving才可以畫呀.. 怎樣在周圍沒有蜘蛛實體的時候畫出蜘蛛呢?
前輩請幫幫我T_T 找了好多資料, 也沒能解決....
SZ大大... 求回復.. Orz
已經解決了......