Games vs level editor

I just realized that the kind of game I write where the player chooses objects from menu’s then places them in the game- that is essentially the same as a very basic level editor- the main difference being in saving the game as a level for a future load.

I know most games in Panda are 3d rpg style where people explore areas and fight monsters- but that the objects are all placed by the programmers.

I tend to play and (try to write) make games where the player places all the objects- or at least all the player controlled items.

So then wouldn’t the process of writing a SPECIFIC Level editor for games like mine would be basically figuring out how to save the level as a text file or *.py file?

I have never used pickles but the python code book says pickles can save objects to loaded later, if I understand that correctly then couldn’t a person utilize a loop to save and load all the objects as pickles then be able to load it later?

JB SKaggs

unfortunately there are several things that cant be pickled in panda. for example nodepath’s and vectors. but doing it yourself is manageable.

i’ve put together a sample how i usually do it, others may have better idea’s.

import pickle, random
from pandac.PandaModules import *

class PandaObject:
  def __init__(self, args):
    self.setData(args)
  
  def destroy(self):
    self.model.removeNode()
  
  def setData(self, data):
    # apply the data
    for name, value in data.items():
      setattr(self, name, value)
    # use the data
    self.model = loader.loadModel(self.modelName)
    self.model.reparentTo(render)
    self.model.setPos(*self.position)
    self.model.setColor(*self.color)
  
  def getData(self):
    # vectors cannot be pickled
    posVec3 = self.model.getPos()
    pos = [posVec3[0], posVec3[1], posVec3[2]]
    colorVec4 = self.model.getColor()
    color = [colorVec4[0], colorVec4[1], colorVec4[2], colorVec4[3]]
    data = {
        'name':self.name,
        'modelName':self.modelName,
        'position':pos,
        'color':color,
      }
    return data

class ObjectManager:
  def __init__(self):
    self.pandaObjects = list()
  
  def createObject(self, data):
    pandaObject = PandaObject(data)
    self.pandaObjects.append(pandaObject)
  
  def saveObjects(self):
    unpickledData = list()
    for pandaObject in self.pandaObjects:
      pandaObjectData = pandaObject.getData()
      unpickledData.append(pandaObjectData)
    pickledData = pickle.dumps(unpickledData)
    file = open('savefile.sav', 'wb')
    file.write(pickledData)
    file.close()
  
  def loadObjects(self):
    file = open('savefile.sav', 'rb')
    pickledData = file.read()
    file.close()
    unpickledData = pickle.loads(pickledData)
    for pandaObjectData in unpickledData:
      self.createObject(pandaObjectData)
  
  def destroyAllObjects(self):
    for pandaObject in self.pandaObjects:
      pandaObject.destroy()
    self.pandaObjects = list()
  
objectManager = ObjectManager()

from direct.directbase import DirectStart

def createNewScene():
  for i in xrange(100):
    pandaObjectData = {
      'name' : 'name-%i'%random.random()*1000,
      'modelName': 'misc/sphere',
      'position': (random.random()*10, random.random()*10, random.random()*10),
      'color': (random.random(), random.random(), random.random(), random.random()),
    }
    objectManager.createObject(pandaObjectData)

if __name__ == '__main__':
  base.accept('n', createNewScene)
  base.accept('s', objectManager.saveObjects)
  base.accept('l', objectManager.loadObjects)
  base.accept('d', objectManager.destroyAllObjects)
  run()

press n to create a new scene
press s to save the scene to a file
press l to load the file
press d to destroy all currently loaded objects

(when loading you should destroy all objects first)

also it’s not recommended to use the pickle built into python, because of security issues. there are replacements that should be more secure. you can easily introduce code into a pickled file (with the default pickling) that will executed when loading the data.

i have modified one of the available picklers to handle panda3d vectors and point’s, i am not sure if i did not introduce security issues. but i’ll share it anyway:

"""
rencode -- Web safe object pickling/unpickling.

The rencode module is a modified version of bencode from the
BitTorrent project.  For complex, heterogeneous data structures with
many small elements, r-encodings take up significantly less space than
b-encodings:

 >>> len(rencode.dumps({'a':0, 'b':[1,2], 'c':99}))
 13
 >>> len(bencode.bencode({'a':0, 'b':[1,2], 'c':99}))
 26

The rencode format is not standardized, and may change with different
rencode module versions, so you should check that you are using the
same rencode version throughout your project.
"""

__version__ = '1.0.0'
__all__ = ['dumps', 'loads']

# Original bencode module by Petru Paler, et al.
#
# Modifications by Reto Spoerri (26.11.2009)
#  - Added some comments
#  - Added Panda3d Vector and Point types
#
# Modifications by Connelly Barnes:
#
#  - Added support for floats (sent as 32-bit or 64-bit in network
#    order), bools, None.
#  - Allowed dict keys to be of any serializable type.
#  - Lists/tuples are always decoded as tuples (thus, tuples can be
#    used as dict keys).
#  - Embedded extra information in the 'typecodes' to save some space.
#  - Added a restriction on integer length, so that malicious hosts
#    cannot pass us large integers which take a long time to decode.
#
# Licensed by Bram Cohen under the "MIT license":
#
#  "Copyright (C) 2001-2002 Bram Cohen
#
#  Permission is hereby granted, free of charge, to any person
#  obtaining a copy of this software and associated documentation files
#  (the "Software"), to deal in the Software without restriction,
#  including without limitation the rights to use, copy, modify, merge,
#  publish, distribute, sublicense, and/or sell copies of the Software,
#  and to permit persons to whom the Software is furnished to do so,
#  subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be
#  included in all copies or substantial portions of the Software.
#
#  The Software is provided "AS IS", without warranty of any kind,
#  express or implied, including but not limited to the warranties of
#  merchantability,  fitness for a particular purpose and
#  noninfringement. In no event shall the  authors or copyright holders
#  be liable for any claim, damages or other liability, whether in an
#  action of contract, tort or otherwise, arising from, out of or in
#  connection with the Software or the use or other dealings in the
#  Software."
#
# (The rencode module is licensed under the above license as well).
#

import struct
import string
import traceback

from pandac.PandaModules import *

# Number of bits for serialized floats, either 32 or 64.
FLOAT_BITS = 32

# Maximum length of integer when written as base 10 string.
MAX_INT_LENGTH = 64

# The bencode 'typecodes' such as i, d, etc have been extended and
# relocated on the base-256 character set.
CHR_LIST  = chr(59)
CHR_DICT  = chr(60)
CHR_INT   = chr(61)
CHR_INT1  = chr(62)
CHR_INT2  = chr(63)
CHR_INT4  = chr(64)
CHR_INT8  = chr(65)
CHR_FLOAT = chr(66)
CHR_TRUE  = chr(67)
CHR_FALSE = chr(68)
CHR_NONE  = chr(69)
CHR_TERM  = chr(127)
# panda3d values
CHR_VEC   = chr(33)
CHR_POINT = chr(34)
CHR_VEC_D   = chr(35)
CHR_POINT_D = chr(36)
CHR_TUPLE  = chr(37)
CHR_VBASE = chr(38)
CHR_VBASE_D = chr(39)

# Positive integers with value embedded in typecode.
INT_POS_FIXED_START = 0
INT_POS_FIXED_COUNT = 32


# NOTE: i've made the start 4 higher and the count 4 smaller, dunno what that will change...
# Negative integers with value embedded in typecode.
INT_NEG_FIXED_START = 70 #74     # ord(CHR_POINT_D) + 1    # last of the lower char's
INT_NEG_FIXED_COUNT = 32 #28     # ord(CHR_TERM) - 1 - INT_NEG_FIXED_START

# Dictionaries with length embedded in typecode.
DICT_FIXED_START = 102       # INT_NEG_FIXED_START + INT_NEG_FIXED_COUNT
DICT_FIXED_COUNT = 25        # ord(CHR_TERM) - 1 - DICT_FIXED_START

# Strings with length embedded in typecode.
STR_FIXED_START = 128        # ord(CHR_TERM) + 1
STR_FIXED_COUNT = 64

# Lists with length embedded in typecode.
LIST_FIXED_START = STR_FIXED_START+STR_FIXED_COUNT
LIST_FIXED_COUNT = 32

# Lists with length embedded in typecode.
TUPLE_FIXED_START = LIST_FIXED_START+LIST_FIXED_COUNT
TUPLE_FIXED_COUNT = 32

def decode_int(x, f):
    f += 1
    newf = x.index(CHR_TERM, f)
    if newf - f >= MAX_INT_LENGTH:
        raise ValueError('overflow')
    try:
        n = int(x[f:newf])
    except (OverflowError, ValueError):
        n = long(x[f:newf])
    if x[f] == '-':
        if x[f + 1] == '0':
            raise ValueError
    elif x[f] == '0' and newf != f+1:
        raise ValueError
    return (n, newf+1)

def decode_intb(x, f):
    f += 1
    return (struct.unpack('!b', x[f:f+1])[0], f+1)

def decode_inth(x, f):
    f += 1
    return (struct.unpack('!h', x[f:f+2])[0], f+2)

def decode_intl(x, f):
    f += 1
    return (struct.unpack('!l', x[f:f+4])[0], f+4)

def decode_intq(x, f):
    f += 1
    return (struct.unpack('!q', x[f:f+8])[0], f+8)

def decode_float(x, f):
    f += 1
    if FLOAT_BITS == 32:
        n = struct.unpack('!f', x[f:f+4])[0]
        return (n, f+4)
    elif FLOAT_BITS == 64:
        n = struct.unpack('!d', x[f:f+8])[0]
        return (n, f+8)
    else:
        raise ValueError

def decode_string(x, f):
    colon = x.index(':', f)
    try:
        n = int(x[f:colon])
    except (OverflowError, ValueError):
        n = long(x[f:colon])
    if x[f] == '0' and colon != f+1:
        raise ValueError
    colon += 1
    return (x[colon:colon+n], colon+n)

def decode_list(x, f):
    r, f = [], f+1
    while x[f] != CHR_TERM:
        v, f = decode_func[x[f]](x, f)
        r.append(v)
    return (r, f + 1)

def decode_tuple(x, f):
    r, f = [], f+1
    while x[f] != CHR_TERM:
        v, f = decode_func[x[f]](x, f)
        r.append(v)
    return (tuple(r), f + 1)

def decode_dict(x, f):
    r, f = {}, f+1
    while x[f] != CHR_TERM:
        k, f = decode_func[x[f]](x, f)
        r[k], f = decode_func[x[f]](x, f)
    return (r, f + 1)

def decode_true(x, f):
  return (True, f+1)

def decode_false(x, f):
  return (False, f+1)

def decode_none(x, f):
  return (None, f+1)

def decode_vec(x, f):
  r, f = [], f+1
  while x[f] != CHR_TERM:
      v, f = decode_func[x[f]](x, f)
      r.append(v)
  if len(r) == 2:
    r = Vec2(*r)
  if len(r) == 3:
    r = Vec3(*r)
  if len(r) == 4:
    r = Vec4(*r)
  return (r, f+1)

def decode_vec_double(x, f):
  r, f = [], f+1
  while x[f] != CHR_TERM:
      v, f = decode_func[x[f]](x, f)
      r.append(v)
  if len(r) == 2:
    r = Vec2D(*r)
  if len(r) == 3:
    r = Vec3D(*r)
  if len(r) == 4:
    r = Vec4D(*r)
  return (r, f+1)

def decode_vbase(x, f):
  r, f = [], f+1
  while x[f] != CHR_TERM:
      v, f = decode_func[x[f]](x, f)
      r.append(v)
  if len(r) == 2:
    r = VBase2(*r)
  if len(r) == 3:
    r = VBase3(*r)
  if len(r) == 4:
    r = VBase4(*r)
  return (r, f+1)

def decode_vbase_double(x, f):
  r, f = [], f+1
  while x[f] != CHR_TERM:
      v, f = decode_func[x[f]](x, f)
      r.append(v)
  if len(r) == 2:
    r = VBase2D(*r)
  if len(r) == 3:
    r = VBase3D(*r)
  if len(r) == 4:
    r = VBase4D(*r)
  return (r, f+1)

def decode_point(x, f):
  r, f = [], f+1
  while x[f] != CHR_TERM:
      v, f = decode_func[x[f]](x, f)
      r.append(v)
  if len(r) == 2:
    r = Point2(*r)
  if len(r) == 3:
    r = Point3(*r)
  if len(r) == 4:
    r = Point4(*r)
  return (r, f+1)

def decode_point_double(x, f):
  r, f = [], f+1
  while x[f] != CHR_TERM:
      v, f = decode_func[x[f]](x, f)
      r.append(v)
  if len(r) == 2:
    r = Point2D(*r)
  if len(r) == 3:
    r = Point3D(*r)
  if len(r) == 4:
    r = Point4D(*r)
  return (r, f+1)

decode_func = {}
decode_func['0'] = decode_string
decode_func['1'] = decode_string
decode_func['2'] = decode_string
decode_func['3'] = decode_string
decode_func['4'] = decode_string
decode_func['5'] = decode_string
decode_func['6'] = decode_string
decode_func['7'] = decode_string
decode_func['8'] = decode_string
decode_func['9'] = decode_string
decode_func[CHR_LIST ] = decode_list
decode_func[CHR_TUPLE] = decode_tuple
decode_func[CHR_DICT ] = decode_dict
decode_func[CHR_INT  ] = decode_int
decode_func[CHR_INT1 ] = decode_intb
decode_func[CHR_INT2 ] = decode_inth
decode_func[CHR_INT4 ] = decode_intl
decode_func[CHR_INT8 ] = decode_intq
decode_func[CHR_FLOAT] = decode_float
decode_func[CHR_TRUE ] = decode_true
decode_func[CHR_FALSE] = decode_false
decode_func[CHR_NONE ] = decode_none
decode_func[CHR_VEC  ] = decode_vec
decode_func[CHR_VEC_D] = decode_vec_double
decode_func[CHR_POINT] = decode_point
decode_func[CHR_POINT_D] = decode_point_double
decode_func[CHR_VBASE]   = decode_vbase
decode_func[CHR_VBASE_D] = decode_vbase_double

def make_fixed_length_string_decoders():
    def make_decoder(slen):
        def f(x, f):
            return (x[f+1:f+1+slen], f+1+slen)
        return f
    for i in range(STR_FIXED_COUNT):
        decode_func[chr(STR_FIXED_START+i)] = make_decoder(i)

make_fixed_length_string_decoders()

def make_fixed_length_list_decoders():
    def make_decoder(slen):
        def f(x, f):
            r, f = [], f+1
            for i in range(slen):
                v, f = decode_func[x[f]](x, f)
                r.append(v)
            return (r, f)
        return f
    for i in range(LIST_FIXED_COUNT):
        decode_func[chr(LIST_FIXED_START+i)] = make_decoder(i)

make_fixed_length_list_decoders()

def make_fixed_length_tuple_decoders():
    def make_decoder(slen):
        def f(x, f):
            r, f = [], f+1
            for i in range(slen):
                v, f = decode_func[x[f]](x, f)
                r.append(v)
            return (tuple(r), f)
        return f
    for i in range(TUPLE_FIXED_COUNT):
        decode_func[chr(TUPLE_FIXED_START+i)] = make_decoder(i)

make_fixed_length_tuple_decoders()

def make_fixed_length_int_decoders():
    def make_decoder(j):
        def f(x, f):
            return (j, f+1)
        return f
    for i in range(INT_POS_FIXED_COUNT):
        decode_func[chr(INT_POS_FIXED_START+i)] = make_decoder(i)
    for i in range(INT_NEG_FIXED_COUNT):
        decode_func[chr(INT_NEG_FIXED_START+i)] = make_decoder(-1-i)

make_fixed_length_int_decoders()

def make_fixed_length_dict_decoders():
    def make_decoder(slen):
        def f(x, f):
            r, f = {}, f+1
            for j in range(slen):
                k, f = decode_func[x[f]](x, f)
                r[k], f = decode_func[x[f]](x, f)
            return (r, f)
        return f
    for i in range(DICT_FIXED_COUNT):
        decode_func[chr(DICT_FIXED_START+i)] = make_decoder(i)

make_fixed_length_dict_decoders()

def encode_dict(x,r):
    r.append(CHR_DICT)
    for k, v in x.items():
        encode_func[type(k)](k, r)
        encode_func[type(v)](v, r)
    r.append(CHR_TERM)


def loads(x):
    try:
        r, l = decode_func[x[0]](x, 0)
    except (IndexError, KeyError):
        traceback.print_exc()
        raise ValueError
    #if l != len(x):
    #    raise ValueError
    return r

from types import StringType, IntType, LongType, DictType, ListType, TupleType, FloatType, NoneType

def encode_int(x, r):
    if 0 <= x < INT_POS_FIXED_COUNT:
        r.append(chr(INT_POS_FIXED_START+x))
    elif -INT_NEG_FIXED_COUNT <= x < 0:
        r.append(chr(INT_NEG_FIXED_START-1-x))
    elif -128 <= x < 128:
        r.extend((CHR_INT1, struct.pack('!b', x)))
    elif -32768 <= x < 32768:
        r.extend((CHR_INT2, struct.pack('!h', x)))
    elif -2147483648 <= x < 2147483648:
        r.extend((CHR_INT4, struct.pack('!l', x)))
    elif -9223372036854775808 <= x < 9223372036854775808:
        r.extend((CHR_INT8, struct.pack('!q', x)))
    else:
        s = str(x)
        if len(s) >= MAX_INT_LENGTH:
            raise ValueError('overflow')
        r.extend((CHR_INT, s, CHR_TERM))

def encode_float(x, r):
    if FLOAT_BITS == 32:
        r.extend((CHR_FLOAT, struct.pack('!f', x)))
    elif FLOAT_BITS == 64:
        r.extend((CHR_FLOAT, struct.pack('!d', x)))
    else:
        raise ValueError

def encode_bool(x, r):
    r.extend({False: CHR_FALSE, True: CHR_TRUE}[bool(x)])

def encode_none(x, r):
    r.extend(CHR_NONE)

def encode_string(x, r):
    if len(x) < STR_FIXED_COUNT:
        r.extend((chr(STR_FIXED_START + len(x)), x))
    else:
        r.extend((str(len(x)), ':', x))

def encode_list(x, r):
    if len(x) < LIST_FIXED_COUNT:
        r.append(chr(LIST_FIXED_START + len(x)))
        for i in x:
            encode_func[type(i)](i, r)
    else:
        r.append(CHR_LIST)
        for i in x:
            encode_func[type(i)](i, r)
        r.append(CHR_TERM)

def encode_tuple(x, r):
    if len(x) < TUPLE_FIXED_COUNT:
        r.append(chr(TUPLE_FIXED_START + len(x)))
        for i in x:
            encode_func[type(i)](i, r)
    else:
        r.append(CHR_TUPLE)
        for i in x:
            encode_func[type(i)](i, r)
        r.append(CHR_TERM)

def encode_dict(x,r):
    if len(x) < DICT_FIXED_COUNT:
        r.append(chr(DICT_FIXED_START + len(x)))
        for k, v in x.items():
            encode_func[type(k)](k, r)
            encode_func[type(v)](v, r)
    else:
        r.append(CHR_DICT)
        for k, v in x.items():
            encode_func[type(k)](k, r)
            encode_func[type(v)](v, r)
        r.append(CHR_TERM)

def encode_vec(x,r):
  r.append(CHR_VEC)
  for f in x:
    encode_float(f,r)
  r.append(CHR_TERM)

def encode_vec_double(x,r):
  r.append(CHR_VEC_D)
  for f in x:
    encode_float(f,r)
  r.append(CHR_TERM)

def encode_point(x,r):
  r.append(CHR_POINT)
  for f in x:
    encode_float(f,r)
  r.append(CHR_TERM)

def encode_point_double(x,r):
  r.append(CHR_POINT_D)
  for f in x:
    encode_float(f,r)
  r.append(CHR_TERM)

def encode_vbase(x,r):
  r.append(CHR_VBASE)
  for f in x:
    encode_float(f,r)
  r.append(CHR_TERM)

def encode_vbase_double(x,r):
  r.append(CHR_VBASE_D)
  for f in x:
    encode_float(f,r)
  r.append(CHR_TERM)

encode_func = {}
encode_func[IntType] = encode_int
encode_func[LongType] = encode_int
encode_func[FloatType] = encode_float
encode_func[StringType] = encode_string
encode_func[ListType] = encode_list
encode_func[TupleType] = encode_tuple
encode_func[DictType] = encode_dict
encode_func[NoneType] = encode_none
encode_func[Vec2] = encode_vec
encode_func[Vec3] = encode_vec
encode_func[Vec4] = encode_vec
encode_func[Vec2D] = encode_vec_double
encode_func[Vec3D] = encode_vec_double
encode_func[Vec4D] = encode_vec_double
encode_func[Point2] = encode_point
encode_func[Point3] = encode_point
encode_func[Point4] = encode_point
encode_func[Point2D] = encode_point_double
encode_func[Point3D] = encode_point_double
encode_func[Point4D] = encode_point_double
encode_func[VBase2] = encode_vbase
encode_func[VBase3] = encode_vbase
encode_func[VBase4] = encode_vbase
encode_func[VBase2D] = encode_vbase_double
encode_func[VBase3D] = encode_vbase_double
encode_func[VBase4D] = encode_vbase_double

try:
    from types import BooleanType
    encode_func[BooleanType] = encode_bool
except ImportError:
    pass

try:
    from types import UnicodeType
    encode_func[UnicodeType] = encode_string
except ImportError:
    pass

def dumps(x):
    r = []
    encode_func[type(x)](x, r)
    return ''.join(r)


def test():
    
    f1 = struct.unpack('!f', struct.pack('!f', 25.5))[0]
    f2 = struct.unpack('!f', struct.pack('!f', 29.3))[0]
    f3 = struct.unpack('!f', struct.pack('!f', -0.6))[0]
    L = (({'a':15, 'bb':f1, 'ccc':f2, '':(f3,(),False,True,'')},('a',10**20),tuple(range(-100000,100000)),'b'*31,'b'*62,'b'*64,2**30,2**33,2**62,2**64,2**30,2**33,2**62,2**64,False,False, True, -1, 2, 0),)
    assert loads(dumps(L)) == L
    assert loads(dumps(-2**31)) == -2**31
    d = dict(zip(range(-100000,100000),range(-100000,100000)))
    d.update({'a':20, 20:40, 40:41, f1:f2, f2:f3, f3:False, False:True, True:False})
    L = (d, {}, {5:6}, {7:7,True:8}, {9:10, 22:39, 49:50, 44: ''})
    assert loads(dumps(L)) == L
    L = ('', 'a'*10, 'a'*100, 'a'*1000, 'a'*10000, 'a'*100000, 'a'*1000000, 'a'*10000000)
    assert loads(dumps(L)) == L
    L = tuple([dict(zip(range(n),range(n))) for n in range(100)]) + ('b',)
    assert loads(dumps(L)) == L
    L = tuple([dict(zip(range(n),range(-n,0))) for n in range(100)]) + ('b',)
    assert loads(dumps(L)) == L
    L = tuple([tuple(range(n)) for n in range(100)]) + ('b',)
    assert loads(dumps(L)) == L
    L = tuple(['a'*n for n in range(1000)]) + ('b',)
    assert loads(dumps(L)) == L
    L = tuple(['a'*n for n in range(1000)]) + (None,True,None)
    assert loads(dumps(L)) == L
    assert loads(dumps(None)) == None
    assert loads(dumps({None:None})) == {None:None}
    L = [
        Vec2(1,2), Vec3(1,2,3), Vec4(1,2,3,4),
        Vec2D(1,2), Vec3D(1,2,3), Vec4D(1,2,3,4),
        Point2(1,2), Point3(1,2,3), Point4(1,2,3,4),
        Point2D(1,2), Point3D(1,2,3), Point4D(1,2,3,4),
        VBase2(1,2), VBase3(1,2,3), VBase4(1,2,3,4),
        VBase2D(1,2), VBase3D(1,2,3), VBase4D(1,2,3,4),
      ]
    assert loads(dumps(L))
    for x in [1,4,8,16,32,64,128,256]:
      v = range(x)
      assert loads(dumps(v)) == v
      v = tuple(range(x))
      assert loads(dumps(v)) == v

try:
    import psyco
    psyco.bind(dumps)
    psyco.bind(loads)
except ImportError:
    pass


if __name__ == '__main__':
  test()

For the record, I’ve recently added code to Panda to support pickling of most Panda objects, including vectors and NodePaths. This will presumably become part of the 1.7 release.

I think people tend to be overly nervous of pickle’s security issues. There’s no reason to fear loading a pickle file within a secure environment. And any environment that’s secure enough to host your program itself is secure enough to host a pickle file–there’s exactly the same danger from loading a pickle file that there is from loading your code.

The only time pickle becomes a security issue is when you have code running in a secure environment that is loading a pickle file from some other, less secure environment. That’s incredibly rare in practice.

David

drwr:
great to hear that, i’m very happy if i dont have to make such workarounds.
what does the pickle store when pickling a nodepath? does it save the whole model?

as a sidenote:
saving can be easily implemented for lots of projects, however saving only the important data is sometimes more difficult. for example if you pickle a scene with hunderts of nodepath’s, each a exact copy of a loaded model you get a heavily bloated save-file. saving only the path to a model that is shipped with a game anyway can be much more efficient.

Indeed it does, which may be more than you intended, as you illustrate in your example. So some intelligence is called for when pickling nodepaths, or indeed any other thing.

David