Implementation of a 2D Top-Down Display for the Unicron 3D Collaborative Virtual Environment

Samat Jain - http://www.cs.nmsu.edu/~sjain/
December 14, 2004
New Mexico State University
CS409
Under Dr. Clinton Jeffery

Abstract

The Unicron Collaborative Virtual Environment (CVE) provides a 3D world in which avatars can interact with each others and objects in their environment. 3D interfaces, however, impose limitations that hinder work and pay in a CVE. An alternative, 2D, top-down display is implemented and integrated into the Unicron CVE to help address these problems.

Introduction

Part of the quest to make computer simulations more realistic is the move to 3D graphics on a 2D screen. Today's 3D graphics can create ever more and more complicated environments, and along with this, they can be that much more complicated to navigate.

A solution to this is to offer a simplified 2D representation, a little mini-map or simplified display of some sort. This enchances usability without detracting from realism or overall intuitivity. Many computer video games take to heart simplified 2D representations to help navigate complex environments. From a tactical flat map presenting the lay of a terrain to a simple radar to locate friends and teammates (or enemies), simplified 2D representations of complex 3D environments is a tried and true addition to an interface's usability.

According to Tory, Moller, Atkins, and Kirkpatrick, combination 2D/3D displays are more efficient than just 2D or 3D displays alone. 3D displays are useful for "approximate navigation and relative positioning" while 2D displays excel at "precise orientation and position tasks." On this consideration alone, addition of a alternate, concurrent 2D display to an existing 3D interface can certainly cause no harm.

Unicron is a Collaborative Virtual Environment (or, CVE) whose final goal is to create a piece of software useful for distance learning applications (and possibly more). Users can download the software on their own machine, and over a network such as the Internet, login as an avatar and connect to a server in which they can interact with other users through their avatars.

The default interface for Unicron is a 3D world where avatars can walk around, raise their hands, enter rooms, and do other actions. This approach poses some barriers in the ability to do certain tasks, as well as creating some technical problems. For example, it is difficult to locate people spatially in a large environment. On the technical side, not everyone has the high-end computers that can be required for 3D rendering.

The solution created in this report is a simplified 2D representation of the Unicron world that can be displayed side-by-side with the 3D view. This 2D top-down display, tersely called the "map window" in the rest of this report, tries to mitigate some of these problems.

Requirements and Design

The map window should be able to accomplish several tasks:

World Representation

It was decided that the map window's actual map would best to be a top-down view of the current "level" (that is, all objects on the same plane) of the world. A top-down review provides in a quick glance the ability to see where all users in the CVE are, where the current user is, and with a good map, how to get to wherever other avatars may be.

Avatar Representation

Simple arrows were selected to represent avatars in the 2D display. The benefits of arrows, besides being easy to draw with Icon's graphics facilities, include the ways arrows can indicate the directions that avatar face and are traveling, as well as providing a quick and easy recognizable representation of something that can change location.

Multiple colors as well as a text label would be used to help discern avatars from each other, as shown below for an avatar whose UID is "ww1."

example of arrow

Alternate movement would be done by handling keyboard events for the arrow keys, as well as the same keys for moving the avatar in the 3D window. This hopefully provides obvious and intuitive control for the user to control their avatar.

Implementation

There are many different ways for implementing a 2D map-display window. Some considerations include:

Maps

Because currently in Unicron, maps are hardcoded in Unicon code rather from separate loadable files, it was decided it was best to go with pre-generated static map images. Specifically, GIF87a images that can be created in any commodity image manipulation program.

To translate from an arbitrary 3D world, there are certain charactistics that need to be known about the map, including:

To avoid having this kind of information hard-coded into the program, "map definition" files were invented. These files, by convention having a .map extension, have each of these values on separate lines (in the order described above).

Avatars

To handle displaying the direction an avatar may face, it was decided to go with a dynamic drawing (using Icon/Unicon's drawing functions) technique rather than use of static images.

The procedure DrawArrow was created to do this drawing. Given a point and an angle, it draws a cursor-looking shape facing in a certain direction. It uses simple trigonometry to determine the points defining the edges of the cursor. Icon's FillPolygon function is then used to create a filled polygon of a certain color, and DrawPolygon to create a higher-contrast black outline around the cursor. Icon's DrawString function is then used to print text next to the cursor, which with this implementation of the map window is the avatar's UID in the current Unicron world.

To test all of these ideas out, a simple demo was made. The demo used as a map the floor plan of the first floor of NMSU's Science Hall, and provided a blue, user-controllable cursor that the user could use to run around Science Hall.

implementation demo, 13K

Integration

Now that essentially all the code for the graphics portion was done, there was the issue of integrating the code for the map window into the existing Unicron project. This transition involved:

Creation of object-oriented code fixated into a single class called MapWindow is easy with the Unicon language. Much of the work in accomplishing the above tasks involved in creating methods and procedures that were simple and had concise operation. These methods and procedures included:

Methods

The methods that the MapWindow class implements:

method toggle_display()

toggle_display() is a method that simply toggles the display of the map window. It is meant to be called from anywhere in the Unicron program--from a chat window command to a 3D window display hot-key. No resources are taken up by the map window until the toggle_display() has been called (that is, not until it has been displayed for the first time).

method initialize( map_name )

initialize() is meant to be a private method only to be used by the MapWindow class. It creates all handles to window and off-screen buffers when requested (i.e. when the window is displayed for the first time). The parameter map_name specifies what map to load for multi-room/level worlds (not well implemented in Unicron as of this writing).

method handle_keyboard_events()

handle_keyboard_events() handles keyboard events that are sent to the map window, that is, any key that is pressed while the map window is on focus. This implementation of the map window includes keys for controlling movement of the avatar on the 2D plane of the map window (which are the same as controlling the avatar in the 3D window) and a toggle key for display of the map window.

method redraw()

redraw() redraws the window, updating the state and position of avatars on the map window. It is meant to be called from the main Unicron program whenever appropriate events happen; these include: the movement of any avatar (local or remote), the creation of a new avatars, and other state changes. This method also has simple drawing code that prepares the offscreen buffer and copies it to the displayed window. Calculations needed to adapt coordinates from the 3D-world to the 2D-space of the map window are also done in this procedure.

procedure DrawArrow( many arguments )

DrawArrow draws the arrows used to represent avatars used with the map-window. It takes any arbitrary buffer (off-screen or on-screen), x and y coordinates, a color, and the angle the arrow should face, and draws an arrow using Icon/Unicon's graphics functions.

Integration into Unicron

There were several hooks that were needed to be made to make the map window work with Unicron. In no specific order, the classes/files that needed to be modified:

class avatar, avatar.icn

The avatar class required three modifications. The first was there there was no easy-to-use method that would return the location of an avatar.

get_location() is a new method that serves this purpose--it returns a table of various location properties, including x, y, and z coordinates, and angle. Future work on Unicron might prompt the need to know room or world location, and the design of this method easily allows access to this information.

Rather than have to chase down every possible invocation of the avatar class's move1 method, which moved an avatar to an arbitrary location, it simpler to add a single line to redraw the map window. This method specifically is used to update the location of avatars on remote machines.

Similarly, the move method also had another line added to redraw the map window. This function, currently in Unicron, is used to move avatars on the local machine being controlled by the current user.

class Camera, camera.icn

One modification was required: in the handle_keyboard method, another case was added for the "m" key--this toggles the display of the map window from the 3D display window.

class Chat_win, chatwin.icn

One modification was made to the handle_network_input method of Chat_win. To the "update" case, which handles the creation of new avatars that have joined the world, a map window redraw invocation was added.

class World, nsh-world.icn

The World class required several house-keeping-type modifications to accomodate the new map window.

The map_win attribute was added to the World class to contain a reference to the MapWindow class. It is through this attribute that redraw() methods are invoked in other classes

In method world_create() the map_win was set to an instance of the MapWindow class, similar as was done for the Camera and Chat_win classes.

To the event_loop method, a case was added for events that were coming from the map window, and sends handling of these events to the MapWindow class's handle_keyboard_events method.

Once the best places for adding these hooks into the main Unicron codebase were found, all that needed to be done is refinement to deal with other minor problems such as coordinate translation and window focus.

Shown below is the final result of this integration. Multiple clients (which could be multiple people on multiple machines over the Internet) movements are tracked.

final implementation

A larger version (JPEG, 524K) is also available.

References

Tory, Melanie; Moller, Torsten; Atkins, M. Stella; Kirkpatrick, Arthur E. "Combining 2D and 3D views for orientation and relative position tasks." Conference on Human Factors in Computing Systems: Proceedings of the 2004 conference on Human factors in computing systems, 73-80.

Griswold, Ralph; Jeffery, Clintonl Townsend, Gregg. Graphics Programming in Icon. San Jose: Peer to Peer Communications, 1998.

Jeffery, Clinton; Mohamed, Shamim; Pereda, Ray; Parlett, Robert. Programming with Unicon. Unpublished manuscript.

Code Listing for map_window.icn

################################################################################
#
# MapWindow class
# December 06, 2004

# Samat Jain <sjain@cs.nmsu.edu>
#
################################################################################

link printf

link "avatar"
link "model"

link "nsh-world"
link "sh118b"

$include "keysyms.icn"

class MapWindow(
  world,  # handles back to the world/avatar owning this client
  avatar,

  window_handle,  # window handle

  is_displayed, # boolean whether map windows is currently being displayed

  conversion_factor, # factor between ft Unicron World coords and map pixels
  starting_x, # starting x and y positions on map
  starting_y,

  buffer_width, buffer_height, # buffer dimensions

  screen_buffer, # off-screen buffer
  map_buffer # map buffer
  )

################################################################################

# Create the map window, initialize buffers, etc
#   map_name - filename to map definition file

# todo: add some error checking when creating things
method initialize( map_name )
  local map_definition_file, map_image_filename

  map_name := "../../dat/maps/sh118b.map"

  # fixme: use real path
  map_definition_file := open(map_name, "r") |
    stop("MapWindow: Error opening map definition file")

  # Read in map parameters (probably more elegant way to do this)

  map_image_filename := read(map_definition_file)
  conversion_factor  := read(map_definition_file)
  starting_x         := read(map_definition_file)
  starting_y         := read(map_definition_file)

  # fixme: use real path
  map_image_filename := "../../dat/maps/sh118b.gif"

  close(map_definition_file)

  map_buffer := open("", "g", "image=" || map_image_filename, "canvas=hidden" )

  buffer_width := WAttrib(map_buffer, "width")
  buffer_height := WAttrib(map_buffer, "height")

  window_handle := open("Map Window", "g", "size=" || buffer_width || "," ||
    buffer_height, "canvas=hidden")
  screen_buffer := open("", "g", "size=" || buffer_width || "," ||
    buffer_height, "canvas=hidden")

  put(world.event_source_list, world.map_win.window_handle)


end # method initialize

################################################################################

# redraws map window and all components
# fixme: redraw() is incredibly sloppy in the way it has magic numbers
#  and accesses class attributes
method redraw()
  local l, map_x, map_y, map_angle

  if /is_displayed then return

  CopyArea(map_buffer, screen_buffer, 0, 0, buffer_width, buffer_height)

  every avatar := !world.TAvatars do {
    l := avatar.get_location()

    # fixme: magic numbers (room location in FHN)
    map_x := starting_x + conversion_factor*(l["x"] -50.6)
    map_y := starting_y + conversion_factor*(l["z"] -15) # where did I get 15?

    map_angle := l["angle"] + 180
    DrawArrow(screen_buffer, avatar.uid, map_x, map_y, map_angle,
      avatar.getcolor(), 100)
  }

  CopyArea(screen_buffer, window_handle, 0, 0, buffer_width, buffer_height)
end # method redraw

################################################################################

# toggle display of map window
method toggle_display()
  if /window_handle then {
    initialize()
  }

  if /is_displayed then {
    is_displayed := 1

    WAttrib(window_handle, "canvas=normal")
    Raise(window_handle)
  } else {
    is_displayed := & null
    WAttrib(window_handle, "canvas=hidden")
  }

  redraw()

  return
end # method display

################################################################################

method handle_keyboard_events()

  if *Pending(window_handle) = 0 then return
  if /avatar then return

  case Event(window_handle) of {
    "t" | Key_Up: { # go forward
      avatar.move(1, &null)
    }
    "g" | Key_Down: { # go backward

      avatar.move(2, &null)
    }
    "f": { # strafe left
      avatar.move(6, &null)
    }
    "h": { # strafe right

      avatar.move(5, &null)
    }
    "q" | Key_Left: { # rotate left
      avatar.move(7, &null)
    }
    "e" | Key_Right: { # rotate right

      avatar.move(8, &null)
    }
    "m": { # toggle map window display
      toggle_display()
    }
  } # case Event

  redraw()

  return
end # method handle_keyboard_events

################################################################################

initially(wrld)
  world := wrld

  is_displayed := &null

  starting_x := 0

  starting_y := 0
  buffer_height := 0
  buffer_width := 0

end # class MapWindow

################################################################################

# Draws the arrow/cursors as well as name labels onto a specified buffer
procedure DrawArrow( win, uid, x, y, angle, color, percent_size )
  local p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r

  r := (percent_size/100.0) * 10
  angle := dtor(angle) + dtor(180)# magic number..

  p1x := x
  p1y := y

  # these are backward to be compatible with the coords given back by
  #  the avatar class
  p2x := r*sin(angle+dtor(140)) + x
  p2y := r*cos(angle+dtor(140)) + y

  p3x := r*sin(angle+dtor(0)) + x
  p3y := r*cos(angle+dtor(0)) + y

  p4x := r*sin(angle+dtor(220)) + x
  p4y := r*cos(angle+dtor(220)) + y

  Fg( win, color)
  FillPolygon( win, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y )
  Fg( win, "black")
  DrawPolygon( win, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y )

  Font(win, "sans, 12")
  Fg(win, "white")
  FillRectangle(win, x+r, y-12, TextWidth(win, uid),12)
  Fg(win, "black")
  DrawString(win, x+r, y, uid)


end # procedure DrawArrow