This shows you the differences between the selected revision and the current version of the page.
| — | clonesource 2008/10/09 13:59 current | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | <code actionscript> | ||
| + | /*------------------------------------------------------------------------ | ||
| + | * Copyright 2007-2008 (c) Dmitri Sviridov, cast3d.com. | ||
| + | * | ||
| + | * 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. | ||
| + | *------------------------------------------------------------------------ */ | ||
| + | /** | ||
| + | * | ||
| + | * @author Dmitri Sviridov - sds | ||
| + | * @version .90 | ||
| + | * @date April, 23 2008 | ||
| + | */ | ||
| + | |||
| + | package { | ||
| + | import flash.display.Sprite; | ||
| + | import flash.display.StageAlign; | ||
| + | import flash.display.StageScaleMode; | ||
| + | import flash.events.*; | ||
| + | import flash.utils.getTimer; | ||
| + | import flash.utils.Timer; | ||
| + | import flash.text.TextField; | ||
| + | import flash.text.TextFieldAutoSize; | ||
| + | import flash.text.TextFieldType; | ||
| + | import flash.display.MovieClip; | ||
| + | import flash.net.URLRequest; | ||
| + | |||
| + | // Cast3D dependencies | ||
| + | import cast3d.core.Cast3d; | ||
| + | import cast3d.core.events.LoadEvent; | ||
| + | import cast3d.loader.Xc3Loader; | ||
| + | import cast3d.utils.controls.*; | ||
| + | import cast3d.utils.manipulators.Manipulator; | ||
| + | import cast3d.utils.manipulators.ppv2.TrackBall; | ||
| + | import cast3d.utils.controllers.NavigationController; | ||
| + | import cast3d.nodes.Node3d; | ||
| + | import cast3d.tracks.Track3d; | ||
| + | import cast3d.geom.Part3d; | ||
| + | import cast3d.geom.Skin3d; | ||
| + | import cast3d.frames.KeyFrame3d; | ||
| + | |||
| + | // Papervision3d dependencies | ||
| + | import org.papervision3d.scenes.Scene3D; | ||
| + | |||
| + | // Import Papervision3D | ||
| + | import org.papervision3d.cameras.*; | ||
| + | import org.papervision3d.scenes.*; | ||
| + | |||
| + | import org.papervision3d.cameras.*; | ||
| + | import org.papervision3d.scenes.*; | ||
| + | import org.papervision3d.lights.*; | ||
| + | import org.papervision3d.render.*; | ||
| + | import org.papervision3d.view.*; | ||
| + | import org.papervision3d.materials.*; | ||
| + | import org.papervision3d.core.proto.MaterialObject3D; | ||
| + | |||
| + | |||
| + | [SWF(backgroundColor="#335566", frameRate="30")] | ||
| + | |||
| + | public class Sample extends Sprite | ||
| + | { | ||
| + | private var manipulator:TrackBall; | ||
| + | private var animator:Cast3d; | ||
| + | private var cp:ControlPanel; | ||
| + | |||
| + | private var scene:Scene3D; | ||
| + | private var camera:Camera3D; | ||
| + | private var viewport:Viewport3D; | ||
| + | private var renderer:BasicRenderEngine; | ||
| + | |||
| + | private var loader:Xc3Loader; | ||
| + | private var loaded:Boolean; | ||
| + | |||
| + | private var statusText:TextField; | ||
| + | private var statusTimer:Timer; | ||
| + | |||
| + | private var _navigations:Array = new Array; | ||
| + | private var _current_nav:int = 0; | ||
| + | |||
| + | public function Sample() | ||
| + | { | ||
| + | setup3DScene(); | ||
| + | setupStage(); | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * Configures the Stage object | ||
| + | */ | ||
| + | private function setupStage(): void | ||
| + | { | ||
| + | this.stage.scaleMode = StageScaleMode.NO_SCALE; | ||
| + | this.stage.align = StageAlign.TOP_LEFT; | ||
| + | } | ||
| + | |||
| + | private function setup3DScene(): void | ||
| + | { | ||
| + | this.setupPpv3D(); | ||
| + | this.setupCast3D(); | ||
| + | this.setupControls(); | ||
| + | this.loadData(); | ||
| + | |||
| + | this.addEventListener(Event.ENTER_FRAME, this.handleEnterFrame); | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * initial setup for Papervision3D. | ||
| + | */ | ||
| + | public function setupPpv3D(): void | ||
| + | { | ||
| + | this.viewport = new Viewport3D(300, 400, true, false,false,false); | ||
| + | addChild( viewport ); | ||
| + | |||
| + | this.scene = new Scene3D(); | ||
| + | this.camera = new Camera3D(); | ||
| + | this.renderer = new BasicRenderEngine(); | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * initial setup for Cast3D. | ||
| + | */ | ||
| + | public function setupCast3D(): void | ||
| + | { | ||
| + | this.loaded = false; | ||
| + | this.animator = new Cast3d(this.scene, this.camera); | ||
| + | this.animator.animationType = Cast3d.ANIMATION_TYPE_BYFRAME; // ANIMATION_TYPE_REAL; // | ||
| + | this.animator.animationStyle = Cast3d.ANIMATION_STYLE_FORWARD; | ||
| + | Cast3d.fps = 22; | ||
| + | this.animator.autoRewind = true; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * Function setups visual animation control panel. | ||
| + | */ | ||
| + | public function setupControls(): void | ||
| + | { | ||
| + | cp = new ControlPanel(animator); | ||
| + | this.stage.addChild(cp); | ||
| + | // cp.visible = false; | ||
| + | |||
| + | statusText = new TextField(); | ||
| + | statusText.textColor = 0x0000ff; | ||
| + | statusText.autoSize = TextFieldAutoSize.LEFT; | ||
| + | statusText.type = TextFieldType.DYNAMIC; | ||
| + | statusText.y = cp.height; | ||
| + | this.stage.addChild(statusText); | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * Function performs 3D data load from a X3c file. | ||
| + | */ | ||
| + | private function loadData(): void | ||
| + | { | ||
| + | var modelpath:String = loaderInfo.parameters.model; | ||
| + | var rpath:String = loaderInfo.parameters.rpath; | ||
| + | |||
| + | this.loader = new Xc3Loader( modelpath ? modelpath: ""); | ||
| + | this.loader.resourcePath = rpath ? rpath : ""; | ||
| + | |||
| + | if (!modelpath || modelpath.length == 0) | ||
| + | { | ||
| + | this.loader = new Xc3Loader("../../cast3dImport/models/walk/fig.xc3"); | ||
| + | this.loader.resourcePath = "../../cast3dImport/models/walk"; | ||
| + | } | ||
| + | |||
| + | statusText.text = "loading file: " + loader.sourceURL; | ||
| + | statusTimer = new Timer(1000, 0.2); | ||
| + | // designates listeners for the interval and completion events | ||
| + | statusTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete); | ||
| + | statusTimer.start(); | ||
| + | |||
| + | this.loader.preCalcMotion = false; | ||
| + | this.loader.addEventListener(LoadEvent.LOAD_COMPLETE, this.cast3dLoadComplete); | ||
| + | this.loader.addEventListener(LoadEvent.LOAD_ERROR, this.cast3dLoadError); | ||
| + | this.loader.addEventListener(LoadEvent.LOAD_PROGRESS, this.cast3dLoadProgress); | ||
| + | // this.loader.portLoader.onCreateColorMaterial = onColorMaterial; | ||
| + | // this.loader.portLoader.onCreateAssetMaterial = onAssetMaterial; | ||
| + | |||
| + | /** Set Call back functions to modify, augumnet or subsitute | ||
| + | * material or/and geometry data. */ | ||
| + | |||
| + | this.loader.load(this.animator.source); | ||
| + | |||
| + | } | ||
| + | |||
| + | private function onColorMaterial (name:String, material:MaterialObject3D):MaterialObject3D | ||
| + | { | ||
| + | material = new WireframeMaterial(0x777700,1); | ||
| + | material.doubleSided = true; | ||
| + | return material; | ||
| + | } | ||
| + | |||
| + | private function onAssetMaterial ( name:String, material:BitmapMaterial):MaterialObject3D | ||
| + | { | ||
| + | return new WireframeMaterial(0x00FF00,1); | ||
| + | } | ||
| + | /** | ||
| + | * Timer handler | ||
| + | */ | ||
| + | private function onTimerComplete(event:TimerEvent):void | ||
| + | { | ||
| + | trace("Time's Up!"); | ||
| + | statusText.textColor = 0xff0000; | ||
| + | statusText.text = "Loading time exceeded 20 nimutes!"; | ||
| + | removeTimer(); | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * At the end of load, removes timer | ||
| + | */ | ||
| + | private function removeTimer():void | ||
| + | { | ||
| + | statusTimer.stop(); | ||
| + | statusTimer.removeEventListener(TimerEvent.TIMER_COMPLETE,onTimerComplete); | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * Handles the ENTER_FRAME event and updates the 3D scene. | ||
| + | */ | ||
| + | private function handleEnterFrame(event: Event): void | ||
| + | { | ||
| + | if (!this.loaded) return; | ||
| + | |||
| + | var time:Number = getTimer(); | ||
| + | |||
| + | // Update cast3D first | ||
| + | this.animator.render(); | ||
| + | |||
| + | // then render scene | ||
| + | this.renderer.renderScene(scene, camera, viewport); | ||
| + | |||
| + | // Update stat data | ||
| + | if (this.animator.source && cp) | ||
| + | { | ||
| + | var frame:int = this.animator.source.currentFrame; | ||
| + | var kframe:int = this.animator.source.currentKeyFrame; | ||
| + | cp.setCurrentFrame(kframe,frame); | ||
| + | cp.setCurrentTime(animator.currentTime); | ||
| + | cp.currentFps = 1000.0/(getTimer() - time); | ||
| + | if (this.manipulator) manipulator.update(); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private function daeLoadComplete(e:Event):void { | ||
| + | trace("loaded"); | ||
| + | // view.singleRender(); | ||
| + | this.manipulator = new TrackBall(this.animator,this.stage, | ||
| + | this.viewport.viewportWidth, this.viewport.viewportHeight, | ||
| + | Manipulator.Y_UP, | ||
| + | Manipulator.X_S,Manipulator.Z_S,Manipulator.NY_S | ||
| + | ); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * Handles the load complete event | ||
| + | */ | ||
| + | private function cast3dLoadComplete(event: LoadEvent): void | ||
| + | { | ||
| + | makeClone(); | ||
| + | setupNavigator(); | ||
| + | |||
| + | trace("cast3dLoadComplete "); | ||
| + | this.manipulator = new TrackBall(this.animator,this.stage, | ||
| + | this.viewport.viewportWidth, this.viewport.viewportHeight, | ||
| + | Manipulator.Y_UP, | ||
| + | Manipulator.X_S,Manipulator.Z_S,Manipulator.NY_S | ||
| + | ); | ||
| + | |||
| + | // manipulator.showCamera = true; | ||
| + | // manipulator.showCOR = true; | ||
| + | cp.manipulator = this.manipulator; | ||
| + | |||
| + | this.loaded = true; | ||
| + | removeTimer(); | ||
| + | if (loader.loaderror.length) | ||
| + | { | ||
| + | statusText.textColor = 0xff0000; | ||
| + | statusText.text = loader.loaderror; | ||
| + | } | ||
| + | else | ||
| + | { | ||
| + | statusText.visible = false; | ||
| + | } | ||
| + | this.animator.play(); | ||
| + | } | ||
| + | |||
| + | private function cast3dLoadProgress(event: LoadEvent): void | ||
| + | { | ||
| + | var n:Number = event.scenesTotal != 0.0 ? event.scenesLoaded/event.scenesTotal : 0; | ||
| + | var percent:int = n*100; | ||
| + | statusText.text = "Loading " + event.file + " ....... "+ percent.toString() + "%"; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * Handles the load Error event | ||
| + | */ | ||
| + | private function cast3dLoadError(event: LoadEvent): void | ||
| + | { | ||
| + | trace("cast3dLoadError ", event.message ); | ||
| + | removeTimer(); | ||
| + | statusText.textColor = 0xff0000; | ||
| + | statusText.text = event.message; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * This functin make a clone of Model node and sets Navigaion Contraller | ||
| + | */ | ||
| + | private function makeClone():void | ||
| + | { | ||
| + | // First lets find the root node of a character | ||
| + | // we know in advance it's Id is 'Cube' | ||
| + | var nodename:String = "Cube"; | ||
| + | var model_node:Node3d = this.animator.source.find(nodename) as Node3d; | ||
| + | if (!model_node) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to make clone. Not found node: " + nodename; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | // next step is to make a clone instance of a character node | ||
| + | var cloned_model_node:Node3d = model_node.clone(); | ||
| + | |||
| + | // A cloned instance of a node if not attached is handing in the air | ||
| + | // To make it visible we need to add it to a scene, whic is KeyFrame | ||
| + | // There is only one KeyFrame ( with infinite duration) and we know Id | ||
| + | // in advance by looking at the file. | ||
| + | var kfname:String = "Scene_kf"; | ||
| + | var kf:KeyFrame3d = this.animator.source.find(kfname) as KeyFrame3d; | ||
| + | if (!kf) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to make clone. Not found keyframe: " + kfname; | ||
| + | return; | ||
| + | } | ||
| + | // adding cloned node to Keyframe | ||
| + | kf.addNode(cloned_model_node); | ||
| + | // This step is also importan to understand. | ||
| + | // Although we added node to a scene, the actual rendering is taking place | ||
| + | // in Rendering engine, in this case papervision3D. So Cast3d is just manupulating | ||
| + | // with transforms and geometry. This step populates rendering engine with newly created | ||
| + | // node's data. Xc3 file loader does it for you, after it's done, you need to do that explicitly. | ||
| + | cloned_model_node.register(this.animator, kf); | ||
| + | |||
| + | // lets create navigation conrloller for new node. | ||
| + | var nc:NavigationController = new NavigationController(cloned_model_node,"navigator"); | ||
| + | _navigations.push(nc); | ||
| + | |||
| + | // We know that "cube" node actually represents Skined geometry, which means | ||
| + | // the motion is controlled by another skeleton node(s), in this case "lowerBack" node is | ||
| + | // root skeleton node( see source file) | ||
| + | nodename = "lowerBack"; | ||
| + | var skeleton_node:Node3d = this.animator.source.find(nodename) as Node3d; | ||
| + | |||
| + | // lets make a clone of that too. Otherwise both original chatecter skin and cloned one | ||
| + | // will be controlled by same skeleton node(s). | ||
| + | var cloned_skeleton_node:Node3d = skeleton_node.clone(); | ||
| + | if (!cloned_skeleton_node) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to set Cloning for node: " + nodename; | ||
| + | return; | ||
| + | } | ||
| + | // add to same scene | ||
| + | kf.addNode(cloned_skeleton_node); | ||
| + | // populate rendering engine | ||
| + | cloned_skeleton_node.register(this.animator, kf); | ||
| + | |||
| + | // This step required only for Skin | ||
| + | var skin:Skin3d = cloned_model_node.part as Skin3d; | ||
| + | if (skin) | ||
| + | { | ||
| + | // The cloned version of skinned node holds the references to original skeleton bones | ||
| + | // Now we need to reassign to the bones of new ( cloned) skeleton nodes | ||
| + | // binding works in the way that it tries to find matching id of old bone in provided skeleton bone branhes | ||
| + | // once it finds it does the replacement. If fails to find any single bone, the whole process fails. | ||
| + | if (!skin.bindBones(cloned_skeleton_node)) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to make clone. Clould not bind skin: " + skin.id + " to node: " + cloned_skeleton_node.id; | ||
| + | return; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | var tarck_id:String; | ||
| + | var motionAlias:String; | ||
| + | |||
| + | // now we add a 'walking' motion which is represented by MotionGroup class instance with id == "lowerBack_motion" | ||
| + | // again, we know that by looking at source file. | ||
| + | tarck_id = "lowerBack_motion"; | ||
| + | motionAlias = "walk"; | ||
| + | if (!nc.addMotion(cloned_skeleton_node, tarck_id, motionAlias)) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to add Motion " + motionAlias + " for track: " + tarck_id; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | // another motion is a 'jump' motion with id == "lowerBack_motionjump" | ||
| + | motionAlias = "jump"; | ||
| + | tarck_id = "lowerBack_motionjump"; | ||
| + | if (!nc.addMotion(cloned_skeleton_node, tarck_id, motionAlias)) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to add Motion " + motionAlias + " for track: " + tarck_id; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | // lets move the newly created model away from intial position so it does not interlap with original | ||
| + | nc.position.x += 5.0; | ||
| + | nc.rotation.x = 0; nc.rotation.y = 0; nc.rotation.z = 1; nc.rotation.w = 60 * Math.PI/180.0 ; | ||
| + | this.stage.addEventListener(KeyboardEvent.KEY_DOWN, this.keyDownHandler); | ||
| + | } | ||
| + | |||
| + | private function setupNavigator():void | ||
| + | { | ||
| + | // First lets find the root node of a character | ||
| + | // we know by looking at source file, its Id is 'Cube' | ||
| + | var nodename:String = "Cube"; | ||
| + | var node:Node3d = this.animator.source.find(nodename) as Node3d; | ||
| + | if (!node) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to set Navigation control for node: " + nodename; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | // create navigation conrloller fo this node. | ||
| + | var nc:NavigationController = new NavigationController(node,"navigator"); | ||
| + | _navigations.push(nc); | ||
| + | |||
| + | // We know that "Cube" node actually represents Skinned geometry, which means | ||
| + | // the motion is controlled by another skeleton node(s), in this case "lowerBack" node is | ||
| + | // root skeleton node( see source file) | ||
| + | nodename = "lowerBack"; | ||
| + | node = this.animator.source.find(nodename) as Node3d; | ||
| + | if (!node) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to set Navigation control for node: " + nodename; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | var tarck_id:String; | ||
| + | var motionAlias:String; | ||
| + | |||
| + | // now we add a 'walking' motion which is represented by MotionGroup class instance with id == "lowerBack_motion" | ||
| + | // again, we know that by looking at source file. | ||
| + | // Notice that 'motion' is produced by different node that we created nvigation controlled, which in that case "cube" | ||
| + | tarck_id = "lowerBack_motion"; | ||
| + | motionAlias = "walk"; | ||
| + | if (!nc.addMotion(node, tarck_id, motionAlias)) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to add Motion " + motionAlias + " for track: " + tarck_id; | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | // another motion | ||
| + | motionAlias = "jump"; | ||
| + | tarck_id = "lowerBack_motionjump"; | ||
| + | if (!nc.addMotion(node, tarck_id, motionAlias)) | ||
| + | { | ||
| + | statusText.visible = true; | ||
| + | statusText.text = "Failed to add Motion " + motionAlias + " for track: " + tarck_id; | ||
| + | return; | ||
| + | } | ||
| + | nc.position.x -= 5.0; | ||
| + | nc.rotation.x = 0; nc.rotation.y = 0; nc.rotation.z = 1; nc.rotation.w = 30 * Math.PI/180.0 ; | ||
| + | |||
| + | this.stage.addEventListener(KeyboardEvent.KEY_DOWN, this.keyDownHandler); | ||
| + | } | ||
| + | |||
| + | public function keyDownHandler( event :KeyboardEvent ):void | ||
| + | { | ||
| + | var nc:NavigationController = _navigations[_current_nav]; | ||
| + | if (!nc) return; | ||
| + | |||
| + | trace(event.target + "(" + event.currentTarget + "): " + event.keyCode + "/" + event.charCode); | ||
| + | switch( event.keyCode ) | ||
| + | { | ||
| + | case 9: // TAB | ||
| + | _current_nav++; | ||
| + | if (_current_nav >= _navigations.length) _current_nav = 0; | ||
| + | break; | ||
| + | |||
| + | case 37: // left | ||
| + | |||
| + | // start walking motion ant rotate model over 1/4 of motion cycle ( which is one step) | ||
| + | // in local coordinates by 30 degree rotation about Z | ||
| + | nc.run("walk",0.25, null, { x:0, y:0, z:1, w: 30.0*Math.PI/180.0 }); | ||
| + | break; | ||
| + | |||
| + | case 38: // up | ||
| + | |||
| + | // start walking motion by moving model over 1/4 of motion cycle ( which is one step) | ||
| + | // and propogation node forward in local coordinates by Y = -0.33 | ||
| + | nc.run("walk",.25,{x:0, y:-.33, z:0}); | ||
| + | break; | ||
| + | |||
| + | case 39: // right | ||
| + | |||
| + | // start walking motion ant rotate model over 1/4 of motion cycle ( which is one step) | ||
| + | // in local coordinates by 30 degree rotation about -Z | ||
| + | nc.run("walk",.25, null, {x:0, y:0, z:-1, w:30.0*Math.PI/180.0 }); | ||
| + | break; | ||
| + | |||
| + | case 40: // down | ||
| + | |||
| + | // start walking motion by moving model over 1/4 of motion cycle ( which is one step) | ||
| + | // and propogation node backwards in local coordinates by Y = 0.33 | ||
| + | // also we reverse timing (last argument) for that motion so characted walks backwards. | ||
| + | nc.run("walk",.25,{x:0, y:.33, z:0}, null, true); | ||
| + | break; | ||
| + | |||
| + | case 32: // space | ||
| + | |||
| + | nc.run("jump",1.0); | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | } | ||
| + | } | ||
| + | |||
| + | </code> | ||