13 年來,我寫了這些糟糕的遊戲代碼
【導讀】:Evan Todd 用風趣幽默的口吻點評了自己從 2004 年以來做遊戲時寫的代碼,有 Java、C++、Python。經過十幾年打怪練級,最近終於完整地做完並推出了自己的第一套遊戲。Todd 今年 26 歲。
在一個獨處的星期五晚上,因急需一些靈感,你決定重溫一些你過去「征服」的程序。舊硬碟緩緩地旋轉著,你瀏覽著過去那些光榮歲月里編寫的代碼。
噢,不! 這根本不是你所期望的。代碼真的有這麼糟糕嗎? 為什麼沒有人告訴你?當時為什麼會喜歡這樣寫?有必要在一個功能中寫這麼多的 goto 嗎?很快,你就關閉了這個項目。 有那麼一瞬間,你甚至考慮刪除它,然後清空硬碟。
以下是我對自己過去的編程經歷中的一些經驗教訓、代碼片段和警告的整理。為了暴露錯誤,我沒有對原有的命名進行修改。
2004 年
這一年我十三歲。這個項目取名《紅月》,這是一個雄心勃勃的第三人稱飛行射擊遊戲。在該項目中,幾乎沒有代碼不是逐字逐句地從《Developing Games in Java》中複製出來的,這樣寫出來的代碼毫無疑問糟糕透了。讓我們來看一個例子。
我想給玩家設計多武器切換功能。具體方案是將武器模型旋轉到玩家模型的內部,用它變換出下一個武器,然後再將它旋轉回來。以下是動畫代碼。 別把它想得太難了。
public void updateAnimation(long eTime) { if(group.getGroup("gun") == null) { group.addGroup((PolygonGroup)gun.clone()); } changeTime -= eTime; if(changing && changeTime = 0; i = Math.Min(this.bindings.Count - 1, i - 1)) this.bindings[i].OnChanged(this); } } }
遊戲中的每個單欄位,甚至最後一個布爾值,都附帶了一個不好控制的動態分配數組。
看一下屬性綁定變更通知的循環,就能夠了解我使用這個模式所遇到的問題了。它必須向後迭代綁定列表,因為綁定可以根據實際需要添加或刪除 UI 元素,導致綁定列表更改。
然而,由於我對數據綁定如此熱愛,所以我在進行數據綁定之前創建了整個遊戲。我將對象分解成組件,並將對象的屬性綁定在一起。很快,事情就失控了。
jump.Add(new Binding(jump.Crouched, player.Character.Crouched)); jump.Add(new TwoWayBinding(player.Character.IsSupported, jump.IsSupported)); jump.Add(new TwoWayBinding(player.Character.HasTraction, jump.HasTraction)); jump.Add(new TwoWayBinding(player.Character.LinearVelocity, jump.LinearVelocity)); jump.Add(new TwoWayBinding(jump.SupportEntity, player.Character.SupportEntity)); jump.Add(new TwoWayBinding(jump.SupportVelocity, player.Character.SupportVelocity)); jump.Add(new Binding(jump.AbsoluteMovementDirection, player.Character.MovementDirection)); jump.Add(new Binding(jump.WallRunState, wallRun.CurrentState)); jump.Add(new Binding(jump.Rotation, rotation.Rotation)); jump.Add(new Binding(jump.Position, transform.Position)); jump.Add(new Binding(jump.FloorPosition, floor)); jump.Add(new Binding(jump.MaxSpeed, player.Character.MaxSpeed)); jump.Add(new Binding(jump.JumpSpeed, player.Character.JumpSpeed)); jump.Add(new Binding(jump.Mass, player.Character.Mass)); jump.Add(new Binding(jump.LastRollKickEnded, rollKickSlide.LastRollKickEnded)); jump.Add(new Binding(jump.WallRunMap, wallRun.WallRunVoxel)); jump.Add(new Binding(jump.WallDirection, wallRun.WallDirection)); jump.Add(new CommandBinding(jump.WalkedOn, footsteps.WalkedOn)); jump.Add(new CommandBinding(jump.DeactivateWallRun, (Action)wallRun.Deactivate)); jump.FallDamage = fallDamage; jump.Predictor = predictor; jump.Bind(model); jump.Add(new TwoWayBinding(wallRun.LastWallRunMap, jump.LastWallRunMap)); jump.Add(new TwoWayBinding(wallRun.LastWallDirection, jump.LastWallDirection)); jump.Add(new TwoWayBinding(rollKickSlide.CanKick, jump.CanKick)); jump.Add(new TwoWayBinding(player.Character.LastSupportedSpeed, jump.LastSupportedSpeed)); wallRun.Add(new Binding(wallRun.IsSwimming, player.Character.IsSwimming)); wallRun.Add(new TwoWayBinding(player.Character.LinearVelocity, wallRun.LinearVelocity)); wallRun.Add(new TwoWayBinding(transform.Position, wallRun.Position)); wallRun.Add(new TwoWayBinding(player.Character.IsSupported, wallRun.IsSupported)); wallRun.Add(new CommandBinding(wallRun.LockRotation, (Action)rotation.Lock)); wallRun.Add(new CommandBinding(wallRun.UpdateLockedRotation, rotation.UpdateLockedRotation)); vault.Add(new CommandBinding(wallRun.Vault, delegate() { vault.Go(true); })); wallRun.Predictor = predictor; wallRun.Add(new Binding(wallRun.Height, player.Character.Height)); wallRun.Add(new Binding(wallRun.JumpSpeed, player.Character.JumpSpeed)); wallRun.Add(new Binding(wallRun.MaxSpeed, player.Character.MaxSpeed)); wallRun.Add(new TwoWayBinding(rotation.Rotation, wallRun.Rotation)); wallRun.Add(new TwoWayBinding(player.Character.AllowUncrouch, wallRun.AllowUncrouch)); wallRun.Add(new TwoWayBinding(player.Character.HasTraction, wallRun.HasTraction)); wallRun.Add(new Binding(wallRun.LastWallJump, jump.LastWallJump)); wallRun.Add(new Binding(player.Character.LastSupportedSpeed, wallRun.LastSupportedSpeed)); player.Add(new Binding(player.Character.WallRunState, wallRun.CurrentState)); input.Bind(rollKickSlide.RollKickButton, settings.RollKick); rollKickSlide.Add(new Binding(rollKickSlide.EnableCrouch, player.EnableCrouch)); rollKickSlide.Add(new Binding(rollKickSlide.Rotation, rotation.Rotation)); rollKickSlide.Add(new Binding(rollKickSlide.IsSwimming, player.Character.IsSwimming)); rollKickSlide.Add(new Binding(rollKickSlide.IsSupported, player.Character.IsSupported)); rollKickSlide.Add(new Binding(rollKickSlide.FloorPosition, floor)); rollKickSlide.Add(new Binding(rollKickSlide.Height, player.Character.Height)); rollKickSlide.Add(new Binding(rollKickSlide.MaxSpeed, player.Character.MaxSpeed)); rollKickSlide.Add(new Binding(rollKickSlide.JumpSpeed, player.Character.JumpSpeed)); rollKickSlide.Add(new Binding(rollKickSlide.SupportVelocity, player.Character.SupportVelocity)); rollKickSlide.Add(new TwoWayBinding(wallRun.EnableEnhancedWallRun, rollKickSlide.EnableEnhancedRollSlide)); rollKickSlide.Add(new TwoWayBinding(player.Character.AllowUncrouch, rollKickSlide.AllowUncrouch)); rollKickSlide.Add(new TwoWayBinding(player.Character.Crouched, rollKickSlide.Crouched)); rollKickSlide.Add(new TwoWayBinding(player.Character.EnableWalking, rollKickSlide.EnableWalking)); rollKickSlide.Add(new TwoWayBinding(player.Character.LinearVelocity, rollKickSlide.LinearVelocity)); rollKickSlide.Add(new TwoWayBinding(transform.Position, rollKickSlide.Position)); rollKickSlide.Predictor = predictor; rollKickSlide.Bind(model); rollKickSlide.VoxelTools = voxelTools; rollKickSlide.Add(new CommandBinding(rollKickSlide.DeactivateWallRun, (Action)wallRun.Deactivate)); rollKickSlide.Add(new CommandBinding(rollKickSlide.Footstep, footsteps.Footstep));
我遇到了一大堆的問題。我創建了綁定循環,導致死循環。我發現初始化順序常常很重要。然而,初始化是數據綁定的噩夢,一些屬性在添加綁定時被多次初始化。
當添加動畫的時候,我發現數據綁定使得在兩個狀態之間動畫化,變得困難和不直觀。不僅僅是我遇到了這個問題。在解釋任何時候運行動畫都必須關閉數據綁定之前,先看一下視頻Netflix 的這個演講——人們侃侃而談 React 是多麼偉大的技術。
I我也意識到需要打開或關閉數據綁定,所以我添加了一個新的變數:
class Binding { public bool Enabled; }
不幸的是,這破壞了數據綁定的目的。我想擺脫 UI 狀態,這段代碼實際上添加了一些。我要怎樣消除這個狀態?
我知道!數據綁定!
class Binding { public Property Enabled = new Property { Value = true }; }
是的,我真的試了一下。 它一直是綁定著的。 我很快意識到這是多麼的瘋狂。
我們該如何改進數據綁定呢? 嘗試讓你的 UI 變成真正的功能性的和狀態無關的。dear imgui是一個很好的例子。儘可能分離行為和狀態。避免那些讓創建狀態變得容易的技術。創建狀態是需要付出代價的。
總結
還有很多很多更尷尬的錯誤需要討論。我發現了另一種「創造性」的方法來避免全局變數;有一段時間,我和閉包糾纏不清;我設計過「實體」、「組件」、「系統」,這種可以是任何東西的對象;我曾試過用多線程的方式運行一個體素引擎,通過在每個使用它的地方進行加鎖。
以下是這些經驗的總結:
提前處理,而不是簡單地將問題留給電腦。
分離行為和狀態。
編寫單一功能函數。
先寫客戶端代碼。
寫無聊的代碼。
以上是我的編程故事。歡迎大家分享自己的編程糗事~


※etcd啟動流程源碼分析筆記
※給go程序添加命令行參數
※JavasSript陷阱之sort
※阿里巴巴最新面試經驗
※路由器的LED燈將允許攻擊者從物理隔離計算機中竊取數據
TAG:推酷 |
※20年前的今天,這款遊戲創造了「神作」這個詞
※三國遊戲一年出了幾百個,這款憑什麼火了3年還賺了41億?
※卧槽,這遊戲玩第1秒我就死了!
※48小時做出來的遊戲是什麼樣的?
※精品手游井噴,2018年才過了一個月,就已經有6款遊戲爆了!
※這才是最高端的玩家:70歲,一個遊戲,一個角色,堅持了13年!
※吃雞已經過時了,這款2018最火爆的遊戲你玩了嗎?
※迎來狗年回首雞年!2017可能錯過的那些好遊戲
※別只吃雞了!2018年還有這些遊戲值得期待
※白忙一年?這個品類可能是2017年遊戲圈最假風口
※為何這款13年前的遊戲,在2018年看起來一點都不過時?
※厲害了我的哥!每天玩20小時遊戲
※即便獲過了大洗牌,遊戲直播在2018年仍面臨這幾個問題
※曾經的10大紅白機遊戲你玩個幾個?看到最後,有人笑了有人哭了
※大作頻出!2018年即將發售的十款喪屍遊戲,你最期待哪一個?
※360萬人4小時只等來古天樂一句話,「擅長」營銷的貪玩遊戲如何結束這場鬧劇?
※沒玩過的快來補!2017年20款高分遊戲盤點,每一個都是口碑神作
※2018年已經過了四分之一 還有哪些遊戲大作要發售?期待大作都在這
※上架第一天下載量超5000萬,這遊戲火了,網友:沒有中文也要玩
※這20多個經典遊戲,讓孩子的認知發育飛起來