Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to implement object persistence by Unity

2025-02-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Internet Technology >

Share

Shulou(Shulou.com)06/01 Report--

Today, I would like to share with you the relevant knowledge points about how to achieve object persistence in Unity. The content is detailed and the logic is clear. I believe most people still know too much about this knowledge, so share this article for your reference. I hope you can get something after reading this article.

This tutorial is made using Unity 2017.3.1p4.

One of the effects

These cubes survived the end of the game.

Create objects on demand

You can create a scene in the Unity editor and fill it with object instances. This allows you to design a fixed level for the game. Objects can have additional behavior, and you can change the state of the scene in playback mode. Typically, a new object instance is created during playback. Fire bullets, generate enemies, random trophies, and so on. Players may even create custom levels within the game.

It's one thing to create something new in the game. Keeping all of this in mind so that players can quit the game and then return to the game is another matter. Unity does not automatically track potential changes for us. We have to do it ourselves.

In this tutorial, we will create a very simple game. All it does is generate a random cube in response to the key. Once we can track cubes between game sessions, we can increase the complexity of the game in future tutorials.

Game logic

Because our game is very simple, we will use a single Game component script to control it. It will generate a cube, for which we will use prefabricated objects. Therefore, it should contain a public field to connect to the prefabricated instance.

Using UnityEngine

Public class Game: MonoBehaviour {

Public Transform prefab;}

Add game objects to the scene and attach this component to the scene. Then create a default cube, convert it to a preform, and provide a reference to it for the game object.

Game settings.

Player input

We will generate cubes based on the player's input, so our game must be able to detect this. We will use Unity's input system to detect keystrokes. Which key should be used to generate the cube? The C key seems appropriate, but we can make it Game-configurable by adding a public KeyCode enumeration field on the inspector. When defining fields by assignment, use C as the default option.

Public KeyCode createKey = KeyCode.C; creates a key set of C.

We can detect whether the key Update has been pressed by querying the static Input class in the method. The Input.GetKeyDown method returns a Boolean value that tells us whether a specific key was pressed in the current frame. If so, we must instantiate our preform.

Void Update () {if (Input.GetKeyDown (createKey)) {Instantiate (prefab);}} when exactly will Input.GetKeyDown return true?

Only during the frame, the state of the key has never been pressed to become pressed because the player pressed it. Typically, the button stays down for a few frames until the player releases the button, but Input.GetKeyDown returns true only during the first frame. Instead, Input.GetKey keeps returning to every frame where true presses the key. Also, Input.GetKeyUp returns true in the frame where the player releases the key.

Random cube

In game mode, every time we press C or any key configured to respond, our game will generate a cube. But it looks like we can only get one cube because they all end up in the same place. So let's randomize the location of each cube we create.

Track the instantiated Transform component so that we can change its local location. Use the static Random.insideUnitSphere property to get a random point, scale it to a radius of five units, and use it as the final location. Because this is more than just trivial instantiation, put its code in a separate CreateObject method and call it when the key is pressed.

Void Update () {if (Input.GetKeyDown (createKey)) {/ / Instantiate (prefab); CreateObject ();}

Void CreateObject () {Transform t = Instantiate (prefab); t.localPosition = Random.insideUnitSphere * 5f;} randomly place the cube.

Now, the cube is generated inside a sphere, not in exactly the same location. They can still overlap, but that's good. However, they are all aligned and do not look interesting. So let's rotate randomly for each cube, using its static Random.rotation property.

Void CreateObject () {Transform t = Instantiate (prefab); t.localPosition = Random.insideUnitSphere * 5f; t.localRotation = Random.rotation;} random rotation.

Finally, we can also change the size of the cube. We will use uniformly scaled cubes, so they are always perfect cubes, but of different sizes. The static Random.Range method can be used to get float random numbers in a certain range. Let's go from a small size of 0.1 cubic meter to a regular size of 1 cubic meter. To use this value for all three dimensions of a scale bar, simply multiply it with Vector3.one and assign the result to the local scale bar.

Void CreateObject () {Transform t = Instantiate (prefab); t.localPosition = Random.insideUnitSphere * 5f; t.localRotation = Random.rotation; t.localScale = Vector3.one * Random.Range (0.1f, 1f);} random unified scale.

Start a new game

If we want to start a new game, we have to exit the game mode and enter the game mode again. But this only works in the Unity editor. Players need to exit our app and start it again before they can play a new game. If we can start a new game while keeping the game mode, so much the better.

We can start a new game by reloading the scene, but this is not necessary. We can destroy all generated cubes. Let's use another configurable key for this, which defaults to N.

Public KeyCode createKey = KeyCode.C; public KeyCode newGameKey = KeyCode.N; the new game key is set to N.

Check at Update to see if this key is pressed, and if so, call a new BeginNewGame method. We can only process one key at a time, so if the C key is not pressed, only the N key is checked.

Void Update () {if (Input.GetKeyDown (createKey)) {CreateObject ();} else if (Input.GetKey (newGameKey)) {BeginNewGame ();}}

Void BeginNewGame () {}

Tracking object

Our game can generate any number of random cubes, all of which are added to the scene. But Game has no memory of what it produced. In order to destroy cubes, we first need to find them. To make this possible, we will track the list of references to the objects it instantiates with Game.

Why not just use GameObject.Find?

For simple cases (it is easy to distinguish between objects and there are not many objects in the scene), this is possible. For larger scenarios, relying on GameObject.Find is a bad idea. GameObject.FindWithTag is better, but it's best to take matters into your own hands if you know you'll need them later.

We can add an array field Game to it and populate it with references, but we don't know in advance how many cubes will be created. Fortunately, the System.Collections.Generic namespace contains a class List that we can use. It works like an array, except that the size is not fixed.

How does the size of the list change dynamically?

Internally, List uses an array to store its contents and initializes it at a certain size. Items added to the list are placed in this array until they are full. If more items are added, the list copies the contents of the entire array to a new and larger array and uses it from now on. We can do this array management manually, but let List handle it for us. Similarly, Unity supports List fields, just as it supports array fields. They can be edited through the inspector, their contents are saved by the editor, and can be recompiled in playback mode.

Using System.Collections.Generic;using UnityEngine

Public class Game: MonoBehaviour {

...

List objects

... }

But we don't need a generic list. We particularly want the Transform reference list. In fact, List insists that we specify the type of its content. List is a generic type, which means that it acts like a template for a specific list class, each for a specific content type. The syntax is List, where T appends the template type to the generic type between angle brackets. In our case, the correct type is List.

List objects

Like an array, we must make sure that we have an instance of a list object before using it. We will do this by creating a new instance in the Awake method. In the case of arrays, we must use new Transform []. But because we are using lists, we have to use the list new List (). This calls the special constructor method of the list class, which can have parameters, which is why we have to append parentheses to the type name.

Void Awake () {objects = new List ();}

Next, Transform every time we instantiate a new list, we add the reference List to the list through the Add method.

Void CreateObject () {Transform t = Instantiate (prefab); t.localPosition = Random.insideUnitSphere * 5f; t.localRotation = Random.rotation; t.localScale = Vector3.one * Random.Range (0.1f, 1f); objects.Add (t); do we have to wait until the end of CreateObject before adding references?

We can add references to the list as soon as we have the list, so we can add it as soon as the Instantiate result is assigned to a local variable. I'm just pointing out at the end that we should only add fully initialized content to the list.

Clear list

Now we can iterate through the list on BeginNewGame and destroy all instantiated game objects. This function is the same as an array, except that the length of the list can be found through its Count property.

Void BeginNewGame () {for (int I = 0; I < objects.Count; iTunes +) {Destroy (objects [I] .gameObject);}}

This leaves us with a list of references to the destroyed object. We must also get rid of them by calling the list Clear method to empty the list.

Void BeginNewGame () {for (int I = 0; I < objects.Count; iTunes +) {Destroy (objects [I] .gameObject);} objects.Clear ();}

Save and load

To support saving and loading during a single playback session, it is sufficient to keep a series of conversion data in memory. Copy the position, rotation, and scale of all cubes at save time, and use the data loaded in memory to reset the game and generate cubes. However, even after the game is terminated, the real save system can still remember the state of the game. This requires that the game state must be kept somewhere outside the game. The most direct way is to store the data in a file.

What about using PlayerPrefs?

As the name implies, PlayerPrefs is designed to consider game settings and preferences, not game status. Although game state can be packaged as a string, it is inefficient, difficult to manage, and cannot be extended.

Save path

The storage location of game files depends on the file system. Unity handles the differences for us, making the folder paths we can use through the Application.persistentDataPath property available. We can get the text string from this property and store it in the savePath field in Awake, so we only need to retrieve it once.

String savePath

Void Awake () {objects = new List (); savePath = Application.persistentDataPath;}

This provides us with the path to the folder instead of the file. We must append a file name to the path. Let's just use saveFile without worrying about the file extension. Whether we should use a forward or backslash to separate the file name from the rest of the path again depends on the operating system. We can use this Path.Combine method to take care of our details. Path is part of the System.IO namespace.

Using System.Collections.Generic;using System.IO;using UnityEngine

Public class Game: MonoBehaviour {

...

Void Awake () {objects = new List (); savePath = Path.Combine (Application.persistentDataPath, "saveFile");}

... }

Open a file for writing

In order to be able to write data to our saved file, we must first open it. It is provided with path parameters through the File.Open method. It also needs to know why we want to open the file. We want to write data to it, create a file if it does not already exist, or replace an existing file. We specify it by providing the second parameter FileMode.Create. Do this with the new Save method.

Void Save () {File.Open (savePath, FileMode.Create);}

File.Open returns a stream of files, which is useless on its own. We need a data stream that can write data. The data must have some format. We will use the most compact uncompressed format, the raw binary data. The System.IO namespace has a BinaryWriter class that makes this possible. Use its constructor method to create a new instance of this class and provide a file stream as a parameter. We don't need to keep a reference to the file stream, so we can directly use the File.Open call as a parameter. We do need to keep the reference to writer, so assign it to the variable.

Void Save () {BinaryWriter writer = new BinaryWriter (File.Open (savePath, FileMode.Create);}

Now we have a binary writer variable named writer, which references a new binary writer. It's a bit too much to use the word "writer" three times in one expression. When we explicitly create a new, it is also unnecessary to explicitly declare the type of the variable. Instead, we can use the var keyword. This implicitly declares the type of the variable to match whatever is assigned to it immediately, in which case the compiler can figure this out.

Void Save () {varwriter = new BinaryWriter (File.Open (savePath, FileMode.Create);}

Now we have a writer variable that references a new binary writer. Its type is obvious.

When should I use var?

The var keyword is grammatical sugar that you don't need to use. Although you can use it anywhere the compiler can infer the meaning of which type, it is best to do so only if it is more readable and the type is clear. Var in these tutorials, I use it only when I declare a variable using the new keyword and assign it to a variable immediately. So var t = new Type can only be expressed formally.

This keyword is useful when var uses language to integrate queries (LINQ) and anonymous types, but this is beyond the scope of this tutorial.

Close the file

If you open the file, you must make sure that it is also closed. You can do this through a Close method, but this is not safe. If there is a problem between opening and closing the file, an exception may be thrown and the execution of the method may be terminated before closing the file. We must handle exceptions carefully to ensure that the file is always closed. Grammatical sugar can simplify the process. Place the declaration and assignment of the writer variable in parentheses, the using keyword in front of it, and a code block after it. This variable is available within the block, just like the iterator variable of the I standard for loop.

Void Save () {using (var writer = new BinaryWriter (File.Open (savePath, FileMode.Create) {}}

This ensures that after code execution exits the block, writer will dispose of all references correctly anyway. This applies to special one-time types, that is, both write and information flow.

How does using work without grammatical sugar?

In our example, it looks like the following code.

Var writer = new BinaryWriter (File.Open (savePath, FileMode.Create); try {… } finally {if (writer! = null) {((IDisposable) writer) .Dispose ();}}

Write data

We can write the data to the file by calling the Write method of writer. You can write a simple value at a time, such as Boolean, integer, and so on. First of all, we only write how many objects are instantiated.

Void Save () {using (var writer = new BinaryWriter (File.Open (savePath, FileMode.Create) {writer.Write (objects.Count);}}

To actually save this data, we must call the Save method. We will control it again with a key, in which case, use S as the default value.

Public KeyCode createKey = KeyCode.C; public KeyCode saveKey = KeyCode.S

...

Void Update () {if (Input.GetKeyDown (createKey)) {CreateObject ();} else if (Input.GetKey (newGameKey)) {BeginNewGame ();} else if (Input.GetKeyDown (saveKey)) {Save ();}} saves the key to S.

Enter game mode, create a few cubes, and then press the key to save the game. This creates a saveFile file on the file system. If you are unsure of the location of the Debug.Log file, you can write the path to the file to the Unity console using.

You will find that the file contains four bytes of data. Opening the file in a text editor does not display any useful information because the data is binary. It may not show anything, or it may interpret the data as weird characters. There are four bytes because this is the size of the integer.

In addition to how many cubes we write, we must also store the transformed data for each cube. We do this by traversing objects and writing their data, one number at a time. For now, we will be limited to their positions. Therefore, write the XQuery Y and Z components of each cube location in this order.

Writer.Write (objects.Count); for (int I = 0; I < objects.Count; iTunes +) {Transform t = objects [I]; writer.Write (t.localPosition.x); writer.Write (t.localPosition.y); writer.Write (t.localPosition.z);} contains files in seven locations, in four-byte blocks. Why not use BinaryFormatter?

While it's convenient to rely on BinaryFormatter, it's not possible to just use the serialized game object hierarchy BinaryFormatter and deserialize it later. The game object hierarchy must be recreated manually. Similarly, we can fully control and understand every piece of data written by ourselves. In addition, writing data manually requires less space and memory, is faster, and can more easily support the evolving save file format. Sometimes, released games will greatly change the stored content after they are updated or expanded. In this way, some of these games will no longer be able to load players' old saved files. Ideally, the game is backward compatible with all its saved file versions.

Loading data

To load the data we just saved, we must open the file again, this time FileMode.Open is the second parameter. Instead of BinaryWriter, we must use BinaryReader. Do this again using the new Load method, and using the declaration again.

Void Load () {using (var reader = new BinaryReader (File.Open (savePath, FileMode.Open) {}}

The first thing we write to the file is the count property of the list, so that's the first thing to read. We do this by reading reder ReadInt32. We have to be clear about what we read, because there are no parameters to make this clear. The suffix 32 indicates the size of the integer, that is, four bytes, or 32 bits. There are also larger and larger integer variants, but we don't use them.

Using (var reader = new BinaryReader (File.Open (savePath, FileMode.Open) {int count = reader.ReadInt32 ();}

After reading the count, we know how many objects are saved. We have to read a lot of locations from the file. The loop does this, reading three floating-point numbers per iteration to get the XMagi Y and Z components of the position vector. The ReadSingle method reads a single-precision float. The ReadDouble method will read double-precision double.

Int count = reader.ReadInt32 (); for (int I = 0; I < count; iTunes +) {Vector3 p; p.x = reader.ReadSingle (); P.Y = reader.ReadSingle (); P.Z = reader.ReadSingle ();}

Use vectors to set the location of the newly instantiated cube and add it to the list.

For (int I = 0; I < count; iTunes +) {Vector3 p; p.x = reader.ReadSingle (); p.y = reader.ReadSingle (); p.z = reader.ReadSingle (); Transform t = Instantiate (prefab); t.localPosition = p; objects.Add (t);}

At this point, we can recreate all the saved cubes, but they will be added to the cubes that already exist in the scene. In order to load the previously saved game correctly, we must reset it before recreating the game. We can do this by calling BeginNewGame before loading the data.

Void Load () {BeginNewGame (); using (var reader = new BinaryReader (File.Open (savePath, FileMode.Open) {... }}

Press a key when Game calls Load, using L-as the default.

Public KeyCode createKey = KeyCode.C; public KeyCode saveKey = KeyCode.S; public KeyCode loadKey = KeyCode.L

...

Void Update () {… Else if (Input.GetKeyDown (saveKey)) {Save ();} else if (Input.GetKeyDown (loadKey)) {Load ();}} load key is set to L.

Players can now save their cubes and load them in the same playback session or in another playback session. But because we only store position data, we do not store the rotation and scale of the cube. As a result, all loaded cubes end up with the default rotation and scale of the preform.

What happens if anything is loaded before saving anything?

You will then try to open a file that does not exist, which will cause an exception. This tutorial will not check whether the file exists or contains valid data, but we will be more careful in future tutorials.

Abstract storage

Although we need to know the details of reading and writing binary data, this is very low-level. Three Write that need to be called to write a single 3D vector. When saving and loading objects, it would be more convenient if we could work at a higher level and read or write the entire 3D vector with a single method call. In addition, it would be nice if we only used ReadInt and ReadFloat without having to worry about all the different variants we didn't use. Finally, it doesn't matter whether the data is stored in binary, plain text, base-64, or other encoding methods. Game doesn't need to know these details.

Game data writing and reading

To hide the details of reading and writing data, we will create our own reader and writer classes. Let's start with writing and name it after GameDataWriter.

GameDataWriter will not extend MonoBehaviour because we will not attach it to the game object. It will act as a wrapper BinaryWriter, so give it a single writer field.

Using System.IO;using UnityEngine

Public class GameDataWriter {

BinaryWriter writer;}

You can create a new custom author type object instance new GameDataWriter (). But it only makes sense if we have to package a writer. Therefore, use the BinaryWriter parameter to create a custom constructor method. This is a method that uses the type name of its class as its own name, and is also used as its return type. It replaces the implicit default constructor method.

Public GameDataWriter (BinaryWriter writer) {}

Although a call to the constructor method produces a new object instance, such methods do not explicitly return anything. Create an object before calling the constructor, and then the object can do any necessary initialization. In our example, this is just a field that assigns the writer parameter to the object. Because I use the same name for both, I must use the this keyword to make it clear that I am referring to the field of the object rather than the parameter.

Public GameDataWriter (BinaryWriter writer) {this.writer = writer;}

The most basic function is to write a float or an int value. Write adds a public method for this by forwarding the call to the actual writer.

Public void Write (float value) {writer.Write (value);}

Public void Write (int value) {writer.Write (value);}

In addition, some methods are added to write a Quaternion- for rotation-and a Vector3. These methods must write all components of their parameters. For quaternions, these are four components.

Public void Write (Quaternion value) {writer.Write (value.x); writer.Write (value.y); writer.Write (value.z); writer.Write (value.w);} public void Write (Vector3 value) {writer.Write (value.x); writer.Write (value.y); writer.Write (value.z);}

Next, GameDataReader creates a new class using the same method as the writer. In this case, we wrap a BinaryReader.

Using System.IO;using UnityEngine

Public class GameDataReader {

BinaryReader reader

Public GameDataReader (BinaryReader reader) {this.reader = reader;}}

Simply name it the ReadFloat and ReadInt methods, which forward the call to ReadSingle and ReadInt32.

Public float ReadFloat () {return reader.ReadSingle ();}

Public int ReadInt () {return reader.ReadInt32 ();}

Also create ReadQuaternion and ReadVector3 methods. Read their components in the same order as writing them.

Public Quaternion ReadQuaternion () {Quaternion value; value.x = reader.ReadSingle (); value.y = reader.ReadSingle (); value.z = reader.ReadSingle (); value.w = reader.ReadSingle (); return value;}

Public Vector3 ReadVector3 () {Vector3 value; value.x = reader.ReadSingle (); value.y = reader.ReadSingle (); value.z = reader.ReadSingle (); return value;}

Persistent object

It is now much easier to write transformed data to a cube in Game. But we can go further. What if Game can simply call the writer.Write (objects [I])? That would be very convenient, but it would require GameDataWriter to know the details of writing game objects. But it's best to keep the writer simple and limit it to the original value and simple structure.

We can reverse this reasoning. Game does not need to know how to save the game object, which is the responsibility of the object itself. All the object needs is the writer to save itself. Game can then use objects [I]. Save (writer).

Our cube is a simple object with no custom components attached. Therefore, the only thing to save is the transform component. Let's create a PersistableObject component script that knows how to save and load this data. It simply extends MonoBehaviour and has a public Save method and Load a method with GameDataWriter or GameDataReader parameters. Let it save the transform position, rotation, and scale, and load them in the same order.

Using UnityEngine

Public class PersistableObject: MonoBehaviour {

Public void Save (GameDataWriter writer) {writer.Write (transform.localPosition); writer.Write (transform.localRotation); writer.Write (transform.localScale);}

Public void Load (GameDataReader reader) {transform.localPosition = reader.ReadVector3 (); transform.localRotation = reader.ReadQuaternion (); transform.localScale = reader.ReadVector3 ();}}

The idea is that only one component, PersistableObject, is attached to a game object that can only be persisted. There is no point in having multiple such components. We can enforce this by adding the DisallowMultipleComponent attribute to the class.

[DisallowMultipleComponent] public class PersistableObject: MonoBehaviour {

... }

Add this component to our cube preform.

Durable prefabricated parts.

Permanent storage

Now that we have a persistent object type, let's also create a PersistentStorage class to hold such an object. It contains the same logic Game as the save and load logic, except that it saves and loads only one instance of PersistableObject and provides it to public Save and Loadmethod through parameters. Set it to MonoBehaviour so that we can attach it to the game object and initialize its save path.

Using System.IO;using UnityEngine

Public class PersistentStorage: MonoBehaviour {

String savePath

Void Awake () {savePath = Path.Combine (Application.persistentDataPath, "saveFile");}

Public void Save (PersistableObject o) {using (var writer = new BinaryWriter (File.Open (savePath, FileMode.Create) {o.Save (new GameDataWriter (writer));}}

Public void Load (PersistableObject o) {using (var reader = new BinaryReader (File.Open (savePath, FileMode.Open) {o.Load (new GameDataReader (reader));}

Add a new game object to the scene where this component is attached. It represents the persistent storage of our games. In theory, we can have multiple such storage objects that store different things or provide access to different storage types. But in this tutorial, we only use this single file to store objects.

Store objects.

Long-lasting game

To use the new persistent object method, we must rewrite the Game. Change the prefab and objects content types to PersistableObject. Adjust the CreateObject to handle this type of change. Then delete all code specific to reading and writing to the file.

Using System.Collections.Generic;//using System.IO;using UnityEngine

Public class Game: MonoBehaviour {

PublicPersistableObjectprefab

...

List objects

/ / string savePath

Void Awake () {objects = new List (); / / savePath = Path.Combine (Application.persistentDataPath, "saveFile");}

Void Update () {… Else if (Input.GetKeyDown (saveKey)) {/ / Save ();} else if (Input.GetKeyDown (loadKey)) {/ / Load ();}} …

Void CreateObject () {PersistableObject o = Instantiate (prefab); Transform t = o.transformational; … Objects.Add (o);}

/ / void Save () {/ / … / /}

/ / void Load () {/ / … / /}}

We will rely on the PersistentStorage instance for Game to handle the details of storing the data. Add a public field of this type storage so that we Game can reference our storage object. To save and load the game state again, we extend Game to PersistableObject. It can then use storage to load and save itself.

Public class Game: PersistableObject {

...

Public PersistentStorage storage

...

Void Update () {if (Input.GetKeyDown (createKey)) {CreateObject ();} else if (Input.GetKeyDown (saveKey)) {storage.Save (this);} else if (Input.GetKeyDown (loadKey)) {BeginNewGame (); storage.Load (this);}}

... }

Connect the storage through the inspector. Also reconnect the preform because its reference is lost due to a change in the type of the field.

The game is connected to prefabricated parts and storage.

Overlay method

Now, when we save and load the game, we will eventually write and read the conversion data for the main game object. It doesn't work. Instead, we must save and load its object list.

I loaded the game object before I saved it, and the location of the game object changed?

If you want to load an older save file at this time, you will eventually misunderstand the data. Counting integers will be mistaken for X positions, X and Y of the first saved position will eventually be used as Y and Z positions, and then rotation will be populated by the next value, and so on. If you save fewer than four locations, the file contains too little data to load the complete transformation. You will then receive an error message complaining that you are trying to read something other than the end of the file.

Instead of relying on the method PersistableObject defined in Save, we must provide a public version of Game's own Save with the GameDataWriter parameter. Here, the convenient way to use the Save object is to write lists as before.

Public void Save (GameDataWriter writer) {writer.Write (objects.Count); for (int I = 0; I < objects.Count; iTunes +) {objects [I] .Save (writer);}}

This is not enough to make it work properly. The compiler complains that Game.Save hides the inherited member PersistableObject.Save. Although Game can use its own version of Save, PersistentStorage only knows PersistableObject.Save. So it will call this method instead of the method Game in. To ensure that Save calls the correct method, we must explicitly declare that we want to override the method that Game inherits from PersistableObject. This is done by adding the override keyword to the method declaration.

Public override void Save (GameDataWriter writer) {… }

However, we can't just cover any method we like. By default, we do not allow this. We must explicitly enable it by adding the virtual keyword to the Save and Load method declarations in PersistableObject.

Public virtual void Save (GameDataWriter writer) {writer.Write (transform.localPosition); writer.Write (transform.localRotation); writer.Write (transform.localScale);}

Public virtual void Load (GameDataReader reader) {transform.localPosition = reader.ReadVector3 (); transform.localRotation = reader.ReadQuaternion (); transform.localScale = reader.ReadVector3 ();}

What is the virtual keyword?

At a very low level, there are actually no objects or methods. There is only data, some of which is used as instructions to be executed by CPU. Unless optimized, the method call becomes an instruction that tells CPU to jump to another data point and continue execution from there. In addition, it may also place some parameter values. Therefore, when PersistentStorage calls the Save method of this PersistableObject type, it becomes an instruction to jump to a fixed location. The instance we pass to it is the PersistableObject subtype, which is not affected at all. The object instance used to call the method is just another parameter.

The virtual keyword changes this practice. Instead of using hard-coded locations, the compiler adds instructions based on the type involved to find the location to which it jumps. Instead of "use this method, so always jump there." Change to "does this type contain a jump target for this method? if so, go there. If not, check its immediate parent type. Repeat until the target is found." This method is called a virtual method, function, or call table. So virtual. It allows a child type to override the functionality of its parent type.

Note that the details of the low-level instructions that end up being executed by CPU can vary greatly, especially when creating a native executable using Unity's IL2CPP. IL2CPP avoids using virtual method tables whenever possible.

PersistentStorage will now finally call our Game.Save method, even if it has been passed to it as a PersistableObject parameter. There are also Game override Load methods.

Public override void Load (GameDataReader reader) {int count = reader.ReadInt (); for (int I = 0; I < count; iTunes +) {PersistableObject o = Instantiate (prefab); o.Load (reader); objects.Add (o);}} contains two converted files. These are all the contents of the article "how Unity implements object persistence". Thank you for reading! I believe you will gain a lot after reading this article. The editor will update different knowledge for you every day. If you want to learn more knowledge, please pay attention to the industry information channel.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Internet Technology

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report