文章目錄 前言 一、1. What is DOTS and why we use it? 1.DOTS包含的主要元素(三件套) 2.Why we use it? 3.Where we use it? (摘自Unity官方) (1)對于AEC(工程建設)應用 (2) 對于汽車應用 (3) 對于游戲獨立開發(fā)者和自由職業(yè)者 (4)對于游戲工作室 4.DOTS的優(yōu)劣(機遇以及風險) 二、DOTS-Man小游戲項目實戰(zhàn) 1.環(huán)境配置 2.游戲設計 3.正式開發(fā) 一些自帶腳本 Component Mono Behaviour System 最后需要進行的一些操作 太棒啦! 3. 參考文檔
前言
DOTS是Unity在17年左右提出的一個概念,其核心是ECS。
提示:以下是本篇文章正文內(nèi)容,下面案例可供參考
一、1. What is DOTS and why we use it?
全稱:(Multi-Thread)Data-Oriented-Tech-Stack (多線程式)數(shù)據(jù)導向型技術(shù)堆棧
1.DOTS包含的主要元素(三件套)
實體組件系統(tǒng)(ECS) - 提供使用面向數(shù)據(jù)的方法進行編碼的框架。在Unity中它通過Entities軟件包進行分發(fā),您可以通過Package Manager來添加編輯器。 C#作業(yè)系統(tǒng) (JobSystem)- 提供一種生成多線程代碼的簡單方法。它通過Jobs軟件包進行分發(fā)。 Burst編譯器 - 可生成快速、優(yōu)化的本機代碼。它通過Burst軟件包進行分發(fā),可通過Package Manager在編輯器中使用。 本機容器 - 屬于ECS數(shù)據(jù)結(jié)構(gòu),可提供對內(nèi)存的控制,值得注意的是Unity專門對內(nèi)存管理進行了一部分優(yōu)化以降低MissCache。
2.Why we use it?
許多并行編程范式,尤其是SIMD(單指令多數(shù)據(jù))型范式,更傾向于使用SoA(結(jié)構(gòu)體數(shù)組)。在CUDA C編程中也普遍傾向于SoA,一維數(shù)據(jù)元素是為全局內(nèi)存的有效合并訪問而預先準備好的,而相同內(nèi)存操作引用的同字段元素在存儲時時彼此相鄰的,使用SoA能夠顯著減少MissCache。 實體組件系統(tǒng)(ECS)提供了一種面向數(shù)據(jù)的編碼設計方法。利用面向數(shù)據(jù)的方法,可以對數(shù)據(jù)結(jié)構(gòu)加以組織,以免出現(xiàn)高速緩存未命中的情況,從而令隨后的數(shù)據(jù)訪問更加高效、快捷。由于面向?qū)ο蟮脑O計并不專注于數(shù)據(jù)的組織,因此高速緩存未命中的情況很常見,這樣就減慢了CPU訪問數(shù)據(jù)的速度,因為它必須頻繁地返回訪問主內(nèi)存中的數(shù)據(jù)。 C#作業(yè)系統(tǒng)可以輕松地用C#編寫快速、并行化的代碼,以充分利用當今的多核處理器。 Burst編譯器會生成高度優(yōu)化的代碼,而這些代碼可以利用您要編譯的平臺硬件。
Tips:
jobsystem和ecs是兩個不同的東西,但是配合起來使用會有1+1>2的效果 burst與ecs的高度適配也使得ecs運行效率很高
3.Where we use it? (摘自Unity官方)
除非您在尋求短期或中期的性能改進,否則很難判定是否需要過渡到DOTS或何時過渡到DOTS。 DOTS幾乎可以為每個應用程序帶來一定程度的性能改進。這其中包括性能、電池使用壽命、迭代及項目可擴展性 。過渡到DOTS不會造成任何性能的下降,但評估過渡到DOTS所增加的費用卻至關重要,尤其是對于那些僅帶來較小改進的項目。 對于所有應用程序而言,DOTS適合處理大量數(shù)據(jù),例如開放式環(huán)境或使用大量相同材料的復雜結(jié)構(gòu)。通過在實例之間共享公共數(shù)據(jù)以減少內(nèi)存訪問,DOTS也同樣適用于重復的元素。 DOTS將來會幫助您開發(fā)高質(zhì)量的內(nèi)容,而不使用DOTS的Unity卻很難做到,這一點務必要考慮清楚。例如,當今的標準游戲和Unity項目已經(jīng)取代了過去的AAA游戲。放眼未來,您需要采用DOTS來保持競爭力。 針對不同的垂直行業(yè),DOTS可以適用于不同的解決方案:
(1)對于AEC(工程建設)應用
DOTS適合處理大型數(shù)據(jù)集并確保內(nèi)容的可擴展性。 DOTS非常適合進行大型交互式地圖和具有大量模型和重復內(nèi)容(例如建筑物和道路)的環(huán)境設計。 DOTS適用于復雜的工程可視化,可大規(guī)模地模擬現(xiàn)實環(huán)境。例如,DOTS非常適合進行粒度級工廠和基礎架構(gòu)設計。
(2) 對于汽車應用
自動駕駛的仿真和可視化 DOTS非常適合進行大型交通和行人模擬,這需要成千上萬的志愿Agent以逼真的方式移動和交互。
(3) 對于游戲獨立開發(fā)者和自由職業(yè)者
DOTS可以幫助您減輕游戲中一些高成本操作的負擔,并有助于提高性能,尤其是對于一些重復性進程。 許多輕量級游戲(例如用于移動設備的游戲)并不能最大限度地提高硬件性能。即使有些游戲能夠做到這一點,但這可能并不是它的主要關注點。不過,隨著游戲的不斷發(fā)展和硬件需求的持續(xù)增加,明智的做法是為將來使用DOTS做好準備。同樣,Project Tiny也提供了使用DOTS開發(fā)較小應用程序和游戲的解決方案。 如果您沒有使用DOTS的迫切需求,那么最好先未雨綢繆,提高自己的DOTS技能,以便在DOTS成為Unity開發(fā)的標準方法時能夠整裝待發(fā)。
(4)對于游戲工作室
當前格式的DOTS可以幫助您逐步達到Unity或其他方式所無法達到的規(guī)模和性能。具體而言,更長的電池使用壽命、溫度控制以及DOTS所提供的代碼可重用性是其主要優(yōu)勢所在。這些方面的性能改進還使您可以開發(fā)更多的低端設備,尤其是在西方市場以外的地區(qū),這些設備會受到一定的硬件限制。 通過讓研發(fā)團隊以DOTS開展工作,可以幫助您逐步了解所能采取的最佳方法,以及哪些最新的功能和領域最具性能優(yōu)勢和發(fā)展影響力。 DOTS并非要取代引擎團隊的作用,而是讓工程師騰出更多精力在自己的專業(yè)領域(例如陰影或著色器)進行創(chuàng)新。
4.DOTS的優(yōu)劣(機遇以及風險)
在改善Unity項目的績效方面,DOTS有著巨大的潛力。 但是,在使用DOTS時需要做出一些考量,它們會影響到項目的時間表、預算和開發(fā)團隊。以下是一些需要與項目優(yōu)先事項進行比較和對比的事項。這些事項可以歸類為風險與機遇。
機遇
改進性能 。默認情況下,我們經(jīng)常使用“性能”一詞來描述DOTS。這是什么意思呢?借助面向數(shù)據(jù)的設計和多線程,DOTS可以顯著提升內(nèi)存、運行時間和電池性能。隨著游戲中顯示的項目數(shù)量不斷增加,提高性能的潛力也隨之上升。相反,對于項目較少的游戲,您會發(fā)現(xiàn)游戲性能的改善程度卻不太明顯。代碼控制 。隨著項目規(guī)模的不斷增大,DOTS可以更好地控制代碼的復雜性。為DOTS編寫的代碼通??梢愿玫胤蛛x關注點。因此,使用DOTS工作時,代碼重構(gòu)、編寫單元測試以及在開發(fā)人員之間分配工作就變得更加容易。
風險
學習成本 。如果您不熟悉DoD,那么面對DOTS時就會有一個學習曲線。盡管DoD在計算機科學領域有著良好的根基并已存在數(shù)年,而且DoD方法與OOP方法也有很大的不同,但DoD本質(zhì)上并不比OOP復雜。ECS是一種不同于當前Unity MonoBehaviour方法的代碼體系架構(gòu),因此學習需要一定的時間。目前,我們認為一名普通的Unity專業(yè)開發(fā)人員平均需要1個月才能熟練使用DOTS。這一準備時間可以被使用DOTS時的代碼質(zhì)量和性能改進所抵消。當然,具體要取決于項目。有限支持 。DOTS當前僅與Unity中一組有限的功能兼容。 最終,DOTS將與Unity的所有功能完全兼容,但我們目前尚無實現(xiàn)完全兼容的時間表。不過,DOTS允許在單個項目中同時使用游戲?qū)ο蠛虳OTS,因此您可以將DOTS用于最頻繁的處理任務,而將非DOTS Unity用于其余任務。過渡 。如果之前的項目是基于Mono開發(fā),那么跟ECS之間的轉(zhuǎn)換可能比較簡單,使用Unity自帶的一些Hybrid工具就可以較為簡單的做到,但是想要把ECS轉(zhuǎn)化為目前常用的Mono的話,我們認為可以做到,但是十分困難,而且也不建議這么做(為什么要嘗試把高效率轉(zhuǎn)為低效率呢)。目前比較推薦的是HybridECS開發(fā),ECS與Mono混合在一起,ECS再配合Jobsystem處理最需要多線程的那一部分。
隨著時間的推移,晶體管電路逐漸接近性能極限,在摩爾定律逐漸失效的今天,人們面臨的數(shù)據(jù)也呈幾何倍數(shù)暴增,我們有理由去發(fā)明并且學習使用一種效率更高,更能完全發(fā)揮硬件性能的軟件編程方式,目前看來也許ECS也許能做到。
二、DOTS-Man小游戲項目實戰(zhàn)
想要熟悉DOTS以及ECS框架,最好還是要上手做一個小項目,使用部分基礎組件,想要熟悉以及精通還需要大量的練習以及使用,開發(fā)過程中要配合官方Entities文檔使用。 Entities最新版本0.17的官方說明文檔
1.環(huán)境配置
如果是Unity2020.X以下版本: windows -> package manager advanced -> show preview package install三件套 (Entities,Jobs,Burst) install其他組件(Hybrid Renderer,Mathematics)
2.游戲設計
我們準備做一個類似Pac-Man的小游戲,主要熟悉Physics包以及Entities的基本使用,所以不會開發(fā)怪物AI之類的,因為使用DOTS開發(fā)所以就叫DOTS-MAN好了。
需求分析
主要功能有:玩家移動,鏡頭跟隨,分數(shù)顯示,因為如果用ECS來修改UGUI的TEXT可能比較麻煩,這里選擇使用HybridECS開發(fā),使用MonoBehaviours開發(fā)一些基礎功能比如鏡頭跟隨以及物體生產(chǎn)之類。
3.正式開發(fā)
一些自帶腳本
在開發(fā)過程中,因為收集物以及玩家還有地形之類的都要有碰撞,但是ECS無法使用object上面的collider之類的組件,所以就要用Entities包自帶的一些腳本。
記得在掛Entities腳本之前刪掉不用的Object腳本,避免混淆以及無意義的空間占用
把Object轉(zhuǎn)化成Entity的腳本: 一般配合一起使用的腳本就是PhysicsShape和PhysicsBody,一個控制物理碰撞的類型,一個控制entity的物理性質(zhì)(例如重力之類的),各個屬性的作用都有明確說明:
添加physicsbody之后碰到List越界報錯問題解決方案: go into YOURPROJECTLibrary/PackageCache/ copy com.unity.collections@0.15.0-preview.21 into YOURPROJECT/Packages/ open com.unity.collections@0.15.0-preview.21\Unity.Collections\NativeList.cs change line 599 from Allocator.None to Allocator.Invalid
Component
組件只有三個,兩個存儲分別存儲移動和旋轉(zhuǎn)的速度,一個負責標記收集物(所以里面沒有數(shù)據(jù)) 要記得把Serializable屬性改為GenerateAuthoringComponent,這樣把component掛上object之后就會把他變成entity。 創(chuàng)建component和system都可以直接使用右鍵 -> create -> ECS進行快速選擇自帶模板
using Unity
. Entities
; [ GenerateAuthoringComponent ]
public struct MoveComponent
: IComponentData
{ public float moveSpeed
;
}
using Unity
. Entities
; [ GenerateAuthoringComponent ]
public struct RotationComponent
: IComponentData
{ public float rotateSpeed
;
}
Component配置: 玩家: 墻體和收集物:
要注意在腳本中配置Collision Filter相關以及Collision Response相關,即某個entity屬于哪個標簽,他能與其他哪些標簽的entity發(fā)生碰撞
搭建一個使用場景(renderer相關的根據(jù)自己喜好來整):
因為mono和ECS是相互穿插的,所以如果mono中有需要的system可以直接先去看看system的代碼,配合官方文檔理解為何這么做,這樣才能把整個流程梳理清楚(至少我學習的時候是這樣的)
Mono Behaviour
這里需要一個全局的mono behaviour來控制游戲,例如entity與object的連接,這里我們換一種方式,把之前的玩家小球弄成prefab,然后在這個全局mono控制玩家的生成,起名就叫做GameManager吧(具體說明看注釋 ):
GameManager:
using System
. Collections
;
using UnityEngine
;
using Unity
. Entities
;
using Unity
. Mathematics
;
using Unity
. Physics
;
using UnityEngine
. UI
;
using Unity
. Transforms
; public class GameManager : MonoBehaviour
{ public static GameManager instance
; public bool insaneMode
; public GameObject ballPrefab
; public GameObject cubePrefab
; public Text scoreText
; public int maxScore
; public int cubesPerFrame
; public float cubeSpeed
= 3f ; private int curScore
; private Entity ballEntityPrefab
; private Entity cubeEntityPrefab
; private EntityManager entityManager
; private BlobAssetStore blobAssetStore
; private void Awake ( ) { if ( instance
!= null && instance
!= this ) { Destroy ( gameObject
) ; return ; } instance
= this ; entityManager
= World
. DefaultGameObjectInjectionWorld
. EntityManager
; blobAssetStore
= new BlobAssetStore ( ) ; GameObjectConversionSettings settings
= GameObjectConversionSettings
. FromWorld ( World
. DefaultGameObjectInjectionWorld
, blobAssetStore
) ; ballEntityPrefab
= GameObjectConversionUtility
. ConvertGameObjectHierarchy ( ballPrefab
, settings
) ; cubeEntityPrefab
= GameObjectConversionUtility
. ConvertGameObjectHierarchy ( cubePrefab
, settings
) ; } private void OnDestroy ( ) { blobAssetStore
. Dispose ( ) ; } private void Start ( ) { curScore
= 0 ; insaneMode
= false ; DisplayScore ( ) ; SpawnBall ( ) ; } private void Update ( ) { if ( insaneMode
) { StartCoroutine ( SpawnLotsOfCubes ( ) ) ; } } IEnumerator SpawnLotsOfCubes ( ) { while ( insaneMode
) { for ( int i
= 0 ; i
< cubesPerFrame
; i
++ ) { SpawnNewCube ( ) ; } yield return null ; } } void SpawnNewCube ( ) { Entity newCubeEntity
= entityManager
. Instantiate ( cubeEntityPrefab
) ; Vector3 direction
= Vector3
. up
; Vector3 speed
= direction
* cubeSpeed
; PhysicsVelocity velocity
= new PhysicsVelocity ( ) { Linear
= speed
, Angular
= float3
. zero
} ; entityManager
. AddComponentData ( newCubeEntity
, velocity
) ; } public void IncreaseScore ( ) { curScore
++ ; DisplayScore ( ) ; } private void DisplayScore ( ) { scoreText
. text
= "Score: " + curScore
; } void SpawnBall ( ) { Entity newBallEntity
= entityManager
. Instantiate ( ballEntityPrefab
) ; Translation ballTrans
= new Translation { Value
= new float3 ( 0f , 0.5f , 0f ) } ; entityManager
. AddComponentData ( newBallEntity
, ballTrans
) ; CameraFollow
. instance
. ballEntity
= newBallEntity
; }
}
CameraFollow: 相機跟隨的mono腳本:
using Unity
. Entities
;
using Unity
. Transforms
;
using Unity
. Mathematics
;
using UnityEngine
; public class CameraFollow : MonoBehaviour
{ public static CameraFollow instance
; public Entity ballEntity
; public float3 offset
; private EntityManager manager
; private void Awake ( ) { if ( instance
!= null && instance
!= this ) { Destroy ( gameObject
) ; return ; } instance
= this ; manager
= World
. DefaultGameObjectInjectionWorld
. EntityManager
; } private void LateUpdate ( ) { if ( ballEntity
== null ) { return ; } Translation ballPos
= manager
. GetComponentData < Translation > ( ballEntity
) ; transform
. position
= ballPos
. Value
+ offset
; }
}
記得把相機腳本掛到main camera上!
System
MoveSystem: 控制玩家移動,獲取玩家輸入放入一個float2中,具體的Mathematics相關class可以看官方文檔,這是一個用起來比vector要快的東西(因為ECS是數(shù)據(jù)驅(qū)動,不用特別關注object):
using Unity
. Entities
;
using Unity
. Jobs
;
using Unity
. Mathematics
;
using Unity
. Physics
;
using UnityEngine
; public class MoveSystem : SystemBase
{ protected override void OnUpdate ( ) { float deltaTime
= Time
. DeltaTime
; float2 curInput
= new float2 ( Input
. GetAxis ( "Horizontal" ) , Input
. GetAxis ( "Vertical" ) ) ; Entities
. ForEach ( ( ref PhysicsVelocity vel
, ref MoveComponent speedData
) = > { float2 newVel
= vel
. Linear
. xz
; newVel
+ = curInput
* speedData
. moveSpeed
* deltaTime
; vel
. Linear
. xz
= newVel
; } ) . Run ( ) ; }
}
相關要點: ForEach就是對包含參數(shù)相關Component的entity在每一幀都進行一定的操作,其中ref關鍵字表示對數(shù)據(jù)進行讀取也可以修改,而in關鍵字表示對數(shù)據(jù)只讀,而且in一定要全部放在ref后面。 后面的的.Run()表示在主線程中運行,如果要在子線程可以使用Schedule。
RotateSystem: 控制收集物旋轉(zhuǎn)的system,具體的quaternion用法可以參考官方文檔:
using Unity
. Entities
;
using Unity
. Jobs
;
using Unity
. Mathematics
;
using Unity
. Transforms
; public class RotateSystem : SystemBase
{ protected override void OnUpdate ( ) { float deltaTime
= Time
. DeltaTime
; Entities
. ForEach ( ( ref Rotation rotation
, in RotationComponent rotationSpeed
) = > { rotation
. Value
= math
. mul ( rotation
. Value
, quaternion
. RotateX ( math
. radians ( rotationSpeed
. rotateSpeed
* deltaTime
) ) ) ; rotation
. Value
= math
. mul ( rotation
. Value
, quaternion
. RotateY ( math
. radians ( rotationSpeed
. rotateSpeed
* deltaTime
) ) ) ; rotation
. Value
= math
. mul ( rotation
. Value
, quaternion
. RotateZ ( math
. radians ( rotationSpeed
. rotateSpeed
* deltaTime
) ) ) ; } ) . Run ( ) ; }
}
記得這時候往你的object上面掛component!如果想讓玩家移動就掛movecomponent,讓收集物旋轉(zhuǎn)就掛上rotationcomponent??梢韵胍幌?#xff0c;如果你往收集物上掛了movecomponent會發(fā)生什么?為什么會這樣?
這時候你的收集物應該是旋轉(zhuǎn)的,玩家小球可以通過wasd或者方向鍵控制移動: 3. CollectSystem: 然后就是最難的碰撞收集系統(tǒng)了!本來在mono中兩三行就可以解決的問題,現(xiàn)在要寫幾十行才能解決!但是對于后期優(yōu)化以及性能上的提升,這些困難都不算什么! 相關的解釋說明都在注釋中了:
using Unity
. Entities
;
using Unity
. Collections
;
using Unity
. Physics
;
using Unity
. Physics
. Systems
; [ UpdateInGroup ( typeof ( FixedStepSimulationSystemGroup
) ) ]
public class CollectSystem : SystemBase
{ private EndFixedStepSimulationEntityCommandBufferSystem bufferSystem
; private BuildPhysicsWorld buildPhysicsWorld
; private StepPhysicsWorld stepPhysicsWorld
; protected override void OnCreate ( ) { bufferSystem
= World
. GetOrCreateSystem < EndFixedStepSimulationEntityCommandBufferSystem > ( ) ; buildPhysicsWorld
= World
. GetOrCreateSystem < BuildPhysicsWorld > ( ) ; stepPhysicsWorld
= World
. GetOrCreateSystem < StepPhysicsWorld > ( ) ; } protected override void OnUpdate ( ) { Dependency
= new TriggerJob { speedEntities
= GetComponentDataFromEntity < MoveComponent > ( ) , entitiesToDelete
= GetComponentDataFromEntity < DeleteTag > ( ) , commandBuffer
= bufferSystem
. CreateCommandBuffer ( ) , } . Schedule ( stepPhysicsWorld
. Simulation
, ref buildPhysicsWorld
. PhysicsWorld
, Dependency
) ; bufferSystem
. AddJobHandleForProducer ( Dependency
) ; } private struct TriggerJob
: ITriggerEventsJob
{ public ComponentDataFromEntity
< MoveComponent
> speedEntities
; [ ReadOnly ] public ComponentDataFromEntity
< DeleteTag
> entitiesToDelete
; public EntityCommandBuffer commandBuffer
; public void Execute ( TriggerEvent triggerEvent
) { TestEntityTrigger ( triggerEvent
. EntityA
, triggerEvent
. EntityB
) ; TestEntityTrigger ( triggerEvent
. EntityB
, triggerEvent
. EntityA
) ; } private void TestEntityTrigger ( Entity entity1
, Entity entity2
) { if ( speedEntities
. HasComponent ( entity1
) ) { if ( entitiesToDelete
. HasComponent ( entity2
) ) { return ; } commandBuffer
. AddComponent < DeleteTag > ( entity2
) ; commandBuffer
. RemoveComponent < PhysicsCollider > ( entity2
) ; } } }
}
DeleteSystem: 控制刪除有deletetag的entity的system:
using Unity
. Entities
; [ UpdateInGroup ( typeof ( FixedStepSimulationSystemGroup
) ) ]
[ UpdateAfter ( typeof ( CollectSystem
) ) ]
public class DeleteSystem : SystemBase
{ private EndFixedStepSimulationEntityCommandBufferSystem _endSimulationECBSystem
; protected override void OnStartRunning ( ) { _endSimulationECBSystem
= World
. GetOrCreateSystem < EndFixedStepSimulationEntityCommandBufferSystem > ( ) ; } protected override void OnUpdate ( ) { var ecb
= _endSimulationECBSystem
. CreateCommandBuffer ( ) ; Entities
. WithAll < DeleteTag > ( ) . WithoutBurst ( ) . ForEach ( ( Entity entity
) = > { GameManager
. instance
. IncreaseScore ( ) ; ecb
. DestroyEntity ( entity
) ; } ) . Run ( ) ; _endSimulationECBSystem
. AddJobHandleForProducer ( Dependency
) ; }
}
這里ForEach之前有一系列限定條件,比如.WithAll()的意思就是對帶有deletetag的entity執(zhí)行下面的操作,這樣能更加方便的進行處理,所以大部分情況下entity都會被打一個標簽來區(qū)別其他entity
最后需要進行的一些操作
創(chuàng)建一個空物體放入GameManager,并且進行相關配置(可以在play模式下打開insaneMode看看ECS的強大性能提升): 在play模式下調(diào)整Camera中的相機跟隨參數(shù),讓鏡頭舒服:
太棒啦!
太棒啦!你成功的使用了目前領先的開發(fā)模式開發(fā)了一個小游戲,雖然這個小游戲的功能在mono中實現(xiàn)的話可以很簡單的實現(xiàn),但是隨著工程規(guī)模的擴大以及性能需求的提高,ECS只會愈發(fā)強大!因為目前DOTS相關教程不完善,所以如果在上述開發(fā)中碰到問題主要需要參考官方文檔以及一些論壇大牛的解答,想要更深入的理解還需要更多項目的磨練。
3. 參考文檔
Entities最新版本0.17的官方說明文檔: https://docs.unity3d.com/Packages/com.unity.entities@0.17/api/Unity.Entities.html 油管ECS大神Turbo的說明文檔: https://www.tmg.dev/tuts/roll-a-ball-entities-0-17/
總結(jié)
以上是生活随笔 為你收集整理的DOTS介绍+Unity DOTS-MAN小游戏项目实战 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。