Json Coder Tutorial

Abstract

Json encoding allows us to safely store and retrieve complex data types in text. This can be used to send objects using the message sensor, as well as interpreting data in ini files.

Prerequisites

Json Overview
Weak Value Dictionary
id
Singleton
Callback

Instructions

Lets say we want to be able to store a bge vector in a text file or send it over the network. We can define a custom json coder to convert the text to a string then convert it back when needed. This is done using a dictionary. The dictionary contains within it the special key word, 'Vector'. This lets the coder know that the contents of the dictionary are a Vector. The text result will look something like this, {"Vector": [1.0, 2.0, 3.0]}. If we need to store a dictionary in a dictionary it appears to be necessary to phrase it differently. Like this, {"Vector": True, "x": 1.0, "y": 2.0, "z":3.0}. Dictionaries within dictionaries seem to work differently.

Rather than hard coding the coder, we can code the strings with callbacks. This is done by placing callback functions in a dict based off the special key word they interpret. We iterate over all the functions untill one returns something. People can easily add new interpreters.

If we want to send arbitrary objects through the message sensor, this can be done by storing there id in a WeakValueDictionay. Our coder creates a WeakValueDictionary for itself. If a custom rule is not found for the objects type, then the object is stored in the dictionary by its id. The custom decoder will then turn this back into the object. Note that this will only work locally for the running instance of the Bge. It can't be used to send objects across the network, or store them in a text file. Such things could still be done, but using different methods.

In python all modules are abstractly a form of singleton. We can use this to make the json coder more accessible. In addition we can structure the module using callbacks so that it can be expanded with out affecting the underling code.

Code

Place this code in a file called jsonCoder.py, located in the same directory as the blend.

"""
an expanded json coder
 
dumps = encode the objects
loads = decode the string
 
encodeAdd/Remove = add/remove a function to the encoding callbacks
decodeAdd/Remove = add/remove a function to the decoding callbacks
 
examples
 
def vectorEncoder(data):
  from mathutils import Vector
  if isinstance(data, Vector):
    return { '__Vector__': data.to_tuple() }
encodeAdd(vectorEncoder, '__Vector__')
 
def vectorDecoder(data):
  if '__Vector__' in data:
    from mathutils import Vector
    return Vector( data['__Vector__'] )
decodeAdd(vectorDecoder, '__Vector__')
"""
# https://docs.python.org/3/library/json.html
import json
# http://upbge.wikidot.com/weakvaluedictionary-tutorial
from weakref import WeakValueDictionary
 
class JsonCoder():
  def __init__(self):
    # We will store a link to an object in a WeakValueDict, based off its id.
    self.refIds = WeakValueDictionary()
    self.encodeCalls = {}
    self.decodeCalls = {}
 
  # These add custom en/decode functions the the coder.
  # The functions are stored with the same key used to indicate its type in the json file.
  def encodeAdd(self, func, key):  self.encodeCalls[key] = func
  def encodeRemove(self, func, key):  self.encodeCalls[key] = func
 
  def decodeAdd(self, func, key):  self.decodeCalls[key] = func
  def decodeRemove(self, func, key):  self.decodeCalls[key] = func
 
  # We can determine the types already registered with this property.
  # That way we can debug any errors.
  @property
  def encodeTypes(self):  return self.encodeCalls.keys()
  @property
  def decodeTypes(self):  return self.decodeCalls.keys()
 
  def default(self, data):
    response = None
    # Run every callback.
    for func in self.encodeCalls.values():
      response = func(data)
      # If we get a response, return it.
      if response:  return response
 
    # If we don't get any responses, store it as a reference id.
    refId = id(data)
    self.refIds[refId] = data
    return { '__RefId__': refId}
 
  def object_hook(self, data):
    response = None
    for func in self.decodeCalls.values():
      response = func(data)
      if response:  return response
 
    if '__RefId__' in data:
      return self.refIds[ data['__RefId__'] ]
 
  def dumps(self, data):  return json.dumps(data, default=self.default)
  def loads(self, data):  return json.loads(data, object_hook=self.object_hook)
 
# We create one instance of this object.
self = JsonCoder()
 
##########
" Vector "
##########
def vectorEncoder(data):
  from mathutils import Vector
  # If its a Vector, store it as a tuple.
  if isinstance(data, Vector):
    return { 'Vector': data.to_tuple() }
# Add the callback to the JsonCoder we created.
self.encodeAdd(vectorEncoder, 'Vector')
 
def vectorDecoder(data):
  if 'Vector' in data:
    from mathutils import Vector
    return Vector( data['Vector'] )
self.decodeAdd(vectorDecoder, 'Vector')
 
########
" Path "
########
# This is a monkey patch that allows us to pass the workingPath to expandPath as part of the callback.
# E.g.  json.workingPath = '~/home/somewhere';  path = jsonCoder.dumps(somePathAsaString)
self.workingPath = None
from expandPath import expandPath
def pathDecoder(data):
  if 'Path' in data:
    path = expandPath(data['Path'], self.workingPath)
    return path
self.decodeAdd(pathDecoder, 'Path')
 
###########
" KeyBind "
###########
# SCA_InputEvent dosnt have a good way of determining if a key is being held down.
# So we subclass it to add a new method.
import bge
class Monkey_SCA_InputEvent(bge.types.SCA_InputEvent):
  """
  bge.types.SCA_InputEvent
 
  |activated  |active  |released  |inactive  |              |
  |           |        |          |          |              |
  |True       |True    |False     |True      |just pressed   |
  |False      |True    |False     |False     |held          |
  |False      |True    |True      |True      |just released |
  |False        |False   |False     |True      |not held      |
 
  its a little weird that its active and inactive are True at the same time.
  """
  # We have to pass it the old object.
  def __init__(self, DNU):  pass
 
  @property
  def held(self):
    # Apparently being released is a form of activity.  Wtevr.
    if self.active and not self.released:  return True
    else:  return False
 
def keyEncoder(data):
  if isinstance(data, bge.types.SCA_InputEvent):
    # We stor the key as {'key': XKEY}.
    return { 'Key': bge.events.EventToString(data.type) }
self.encodeAdd(keyEncoder, 'Key')
 
def keyDecoder(data):
  if 'Key' in data:
    # We return the relevant key event for use as a keybind.
    keyName = data['Key']
    bind = getattr(bge.events, keyName)
    bind = bge.logic.keyboard.inputs[bind]
    bind = Monkey_SCA_InputEvent(bind)
    return bind
self.decodeAdd(keyDecoder, 'Key')
 
# Override this module with our custom object.
# https://sohliloquies.blogspot.com/2017/07/how-to-make-subscriptable-module-in.html
import sys
sys.modules[__name__] = self

Usage

import jsonCoder as json
from mathutils import Vector
import bge
own = bge.logic.getCurrentController().owner
 
msg0 = [1, 2, 3]
vect = Vector(msg0)
 
msg1 = json.dumps(vect)
msg1 = json.loads(msg1)
 
msg2 = json.dumps(own)
msg2 = json.loads(msg2)
 
print( msg1, type(msg1), msg2, type(msg2) )

Questions

Edit this post to ask or answer questions.

Further Reading

Setting Setter
Sheet Syllabus
Json
Monkey Patch
Make a Subscriptable Module
Hard Code

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License