AS3 Error #1009: Cannot access a property or method of a null object reference - actionscript-3

Basically, I'm trying to make a randomly generated character follow a series of waypoints to get to where he needs to go without walking into walls etc on stage.
I'm doing this by passing an Array of Points from the Engine to the Character's followPath function (this will be on a loop, but I haven't gotten to that stage yet).
A part of this followPath function is to detect when the character is close enough to the waypoint and then move on to the next one. To do this, I'm trying to use Point.distance(p1,p2) to calculate the distance between the currently selected waypoint, and a point that represents the Character's current position.
This is where I'm running into this problem. Trying to update the current (x,y) point position of the Character is proving difficult. For some reason, the Point.setTo function does not seem to exist, despite it being in documentation. As a result, I'm using
currentPos.x = x;
currentPos.y = y;
//update current position point x and y
to try and do this, which is where my 1009 error is coming from.
Here is my full Character class so far:
package {
import flash.display.MovieClip;
import flash.geom.Point;
public class Character extends MovieClip {
public var charType:String;
private var charList:Array = ["oldguy","cloudhead","tvhead","fatguy","speakerhead"];
private var numChars:int = charList.length;
private var wpIndex:int = 0;
private var waypoint:Point;
private var currentPos:Point;
private var wpDist:Number;
private var moveSpeed:Number = 5;
//frame labels we will need: charType+["_walkingfront", "_walkingside", "_walkingback", "_touchon", "_touchonreaction", "_sitting"/"_idle", "_reaction1", "_reaction2", "_reaction3", "_reaction4"]
public function Character() {
trace("new character:");
charType = charList[Math.floor(Math.random()*(numChars))];
//chooses a random character type based on a random number from 0-'numchars'
trace("char type: " + charType);
gotoAndStop(charType+"_walkingfront");
x = 600;
y = 240;
}
public function followPath(path:Array):void {
if(wpIndex > path.length){ //if the path has been finished
gotoAndStop(charType+"_sitting"); //sit down
return;//quit
}
waypoint = path[wpIndex];
//choose the selected waypoint
currentPos.x = x;
currentPos.y = y;
//update current position point x and y
wpDist = Point.distance(currentPos,waypoint);
//calculate distance
if(wpDist < 3){ //if the character is close enough to the waypoint
wpIndex++; //go to the next waypoint
return; //stop for now
}
moveTo(waypoint);
}
public function moveTo(wp:Point):void {
if(wp.x > currentPos.x){
currentPos.x += moveSpeed;
} else if(wp.x < currentPos.x){
currentPos.x -= moveSpeed;
}
if(wp.y > currentPos.y){
currentPos.y += moveSpeed;
} else if(wp.y < currentPos.y){
currentPos.y -= moveSpeed;
}
}
}
}
Can anybody explain to me why this is happening? It's a roadblock that I haven't been able to overcome at this stage.
I'm also curious if anybody can provide information as to why I can't use the phantom Point.setTo method.

You're trying to assign x and y properties of a Point object that doesn't exist.
You have to create your Point:
currentPos = new Point ();
and then assign x and y

The problem is that your are not using the Point constructor first.
When you create a variable that is not a simple data type (Int, Number, String ...) you must call the constructor first and assign properties to the fields of the object only afterwards.
This is because you must initialize an instance of the class Point before accessing it's properties.
The same will be true with any other class.
http://en.wikipedia.org/wiki/Constructor_%28object-oriented_programming%29
"In object-oriented programming, a constructor (sometimes shortened to ctor) in a class is a special type of subroutine called at the creation of an object. It prepares the new object for use.."
Basically, you did not prepare a new Point object.
In this example, during the constructor (public function Character)
public function Character() {
//add these lines (you can omit the zeroes as the default value is zero)
//I added the zeroes to show that the constructor can set the initial values.
wayPoint = new Point(0, 0);
currentPos = new Point(0, 0);
trace("new character:");
charType = charList[Math.floor(Math.random()*(numChars))];
//chooses a random character type based on a random number from 0-'numchars'
trace("char type: " + charType);
gotoAndStop(charType+"_walkingfront");
x = 600;
y = 240;
}
remember every new object identifer references NULL (nothing) until you construct an object or do something like this
var pointA = pointB;
//where pointB is already not null
//You can also check this
if(currentPos != null)
{
currentPos.x = X;
currentPos.y = Y;
}
currentPos will not be null if you use a constructor first.
Good luck.

Related

How to use the Timestamp data from JSON file in Unity

I'm currently working with a folder of JSON files which are collected through a tracking experiment with a drone. The data contains position, rotation and timestamp of the drone while it's moving and levitating inside the tracking system.
What I'm currently doing is trying to simulate the movement of the drone inside Unity using those data. So far, I've managed to parse the position and rotation from the data to an object inside Unity and extract the timestamp to System.DateTime in Unity.
However, I don't how to work with the timestamp. I want to use the timestamp to match the position and rotation of the object (i.e: at this timestamp, the drone should be at this position(x,y,z) and has the rotation(x,y,z,w)). Can someone help me with this problem, really appreciate your help :D Here is my current code:
void Update()
{
if (loaded)
{
for(int i = 0; i <= pos_data.Count; i+= 10)
{
Cube.transform.position = pos_data[i];
Cube.transform.rotation = rot_data[i];
}
}
else
{
LoadJson();
//startTime = datetime[0];
loaded = true;
}
}
public void LoadJson()
{
string HeadPath = #Application.dataPath + "/Data/" + "drone_data_1.json";
string HeadJsonhold = File.ReadAllText(HeadPath);
var data_ = JSON.Parse(HeadJsonhold);
for (int rows = 0; rows <= data_.Count; rows += 10)
{
pos_data.Add(new Vector3(data_[rows]["location"]["x"].AsFloat, data_[rows]["location"]["y"].AsFloat, data_[rows]["location"]["z"].AsFloat));
rot_data.Add(new Quaternion(data_[rows]["rotation"]["x"].AsFloat, data_[rows]["rotation"]["y"].AsFloat, data_[rows]["rotation"]["z"].AsFloat, data_[rows]["rotation"]["w"].AsFloat));
Time = System.DateTime.ParseExact(data_[rows]["Timestamp"], "yyyyMMddHHmmss",null);
//Debug.Log(Time);
}
}
If I understand you correctly what you are getting are samples of a real-world drone that at some rate stores keyframes of its movement.
Now you already successfully load that json data but wonder how to animate the Unity object accordingly.
The timestamp itself you can't use at all! ^^
It most probably lies somewhere in the past ;) And you can't just assign something to Time.
What you can do, however, is take the timestamp of the first sample (I will just assume that your samples are all already ordered by the time) and calculate the difference to the next sample and so on.
Then you can use that difference in order to always interpolate between the current and next sample transforms using the given time delta.
Currently you are just doing all samples in one single frame so there won't be any animation at all.
Also just as sidenote:
for(int i = 0; i <= pos_data.Count; i+= 10)
is wrong twice:
a) you already skipped 10 samples when loading the data -> are you sure you now want to again skip 10 of these => In total every time skipping 100 samples?
b) since indices are 0 based the last accessible index would be pos_data.Count - 1 so in general when iterating Lists/arrays it should be i < pos_data.Count ;)
First of all I would suggest you use a better data structure and use one single list holding the information that belongs together instead of multiple parallel lists and rather load your json like e.g.
[Serializable]
public class Sample
{
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public readonly float TimeDelta;
public Sample(Vector3 position, Quaternion rotation, float timeDelta)
{
Position = position;
Rotation = rotation;
TimeDelta = timeDelta;
}
}
And then
// Just making this serialized so you can immediately see in the Inspector
// if your data loaded correctly
[SerializeField] private readonly List<Sample> _samples = new List<Sample>();
public void LoadJson()
{
// start fresh
_samples.Clear();
// See https://learn.microsoft.com/dotnet/api/system.io.path.combine
var path = Path.Combine(Application.dataPath, "Data", "drone_data_1.json");
var json = File.ReadAllText(path);
var data = JSON.Parse(json);
DateTime lastTime = default;
for (var i = 0; i <= data.Count; i += 10)
{
// First I would pre-cache these values
var sample = data[i];
var sampleLocation = sample["location"];
var sampleRotation = sample["rotation"];
var sampleTime = sample["Timestamp"];
// Get your values as you did already
var position = new Vector3(sampleLocation["x"].AsFloat, sampleLocation["y"].AsFloat, sampleLocation["z"].AsFloat));
var rotation = new Quaternion(sampleRotation["x"].AsFloat, sampleRotation["y"].AsFloat, sampleRotation["z"].AsFloat, sampleRotation["w"].AsFloat));
var time = System.DateTime.ParseExact(sampleTime, "yyyyMMddHHmmss", null);
// Now for the first sample there is no deltaTime
// for all others calculate the difference in seconds between the
// last and current sample
// See https://learn.microsoft.com/dotnet/csharp/language-reference/operators/conditional-operator
var deltaTime = i == 0 ? 0f : GetDeltaSeconds(lastTime, time);
// and of course store it for the next iteration
lastTime = time;
// Now you can finally add the sample to the list of samples
// instead of having multiple parallel lists
_samples.Add(new Sample(position, rotation, deltaTime));
}
}
private float GetDeltaSeconds(DateTime first, DateTime second)
{
// See https://learn.microsoft.com/dotnet/api/system.datetime.op_subtraction#System_DateTime_op_Subtraction_System_DateTime_System_DateTime_
var deltaSpan = second - first;
// See https://learn.microsoft.com/dotnet/api/system.timespan.totalseconds#System_TimeSpan_TotalSeconds
return (float)deltaSpan.TotalSeconds;
}
So now what to do with this information?
You now have samples (still assuming ordered by time) holding all required information to be able to interpolate between them.
I would use Coroutines instead of Update, in my eyes they are easier to understand and maintain
// Do your loading **once** in Start
private void Start()
{
LoadJson();
// Then start the animation routine
// I just make it a method so you could also start it later e.g. via button etc
StartAnimation();
}
// A flag just in case to avoid concurrent animations
private bool alreadyAnimating;
// As said just making this a method so you could also remove it from Start
// and call it in any other moment you like
public void StartAnimation()
{
// Only start an animation if there isn't already one running
// See https://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html
if(!alreadyAnimating) StartCoroutine(AnimationRoutine());
}
private IEnumerator AnimationRoutine()
{
// Just in case abort if there is already another animation running
if(alreadyAnimating) yield break;
// Block concurrent routine
alreadyAnimating = true;
// Initially set your object to the first sample
var lastSample = _samples[0];
Cube.transform.position = lastSample.Position;
Cube.transform.rotation = lastSample.Rotation;
// This tells Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
// then iterate through the rest of samples
for(var i = 1; i < _samples.Count; i++)
{
var lastPosition = lastSample.Position;
var lastRottaion = lastSample.Rottaion;
var currentSample = _samples[i];
var targetPosition = sample.Position;
var targetRotation = sample.Rotation;
// How long this interpolation/animation will take
var duration = currentSample.TimeDelta;
// You never know ;)
// See https://docs.unity3d.com/ScriptReference/Mathf.Approximately.html
if(Mathf.Approximately(duration, 0f))
{
Cube.transform.position = targetPosition;
Cube.transform.rotation = targetRotation;
lastSample = currentSample;
continue;
}
// And this is where the animation magic happens
var timePassed = 0f;
while(timePassed < duration)
{
// this factor will be growing linear between 0 and 1
var factor = timePassed / duration;
// Interpolate between the "current" transforms (the ones it had at beginning of this iteration)
// towards the next sample target transforms using the factor between 0 and 1
// See https://docs.unity3d.com/ScriptReference/Vector3.Lerp.html
Cube.transform.position = Vector3.Lerp(lastPosition, targetPosition, factor);
// See https://docs.unity3d.com/ScriptReference/Quaternion.Slerp.html
Cube.transform.rotation = Quaternion.Slerp(lastRotation, targetRotation, factor);
// This tells Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
// increase by the time passed since the last frame was rendered
timePassed += Time.deltaTime;
}
// just to be sure to end with clean values
Cube.transform.position = targetPosition;
Cube.transform.rotation = targetRotation;
lastSample = currentSample;
}
// Allow the next animation to start (or restart this one)
alreadyAnimating = false;
// Additional stuff to do once the animation is done
}

Prevent Movie Clip Overlapsing

I'm creating a screen saver which requires a movie clip to be loaded to the stage at a random position based on the size of the screen, then fade out (which I have all of the animation within a movie clip as tweens)
I've hit a road block and can't figure out how to prevent the movie clips from overlapsing on top of each other. If anything, I'd like for them to appear at another random spot that does not cause the overlapse.
here is all of the code for my project:
stop();
import flash.display.MovieClip;
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.events.Event;
import flash.display.Stage;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
var greystoneLogos:Array = new Array ;
var countTimeArray:Array = new Array ;
var previousLogos:Array = new Array ;
var xpoint:int;
var ypoint:int;
function getNewSymbols()
{
previousLogos = new Array ;
greystoneLogos = new Array ;
var i:int;
for (i=0; i < 3; i++)
{
greystoneLogos[i] = new GreystoneLogo1();
greystoneLogos[i].width = 100;
greystoneLogos[i].height = 60;
addSymbolToStage(greystoneLogos[i],i*2000);
}
}
getNewSymbols();
function addSymbolToStage(currentLogo:MovieClip,waitTime:int)
{
var i3:int;
var i4:int;
var logoBoundaries:Array = new Array()
var XandY:Array = new Array()
for (i3=0; i3 < greystoneLogos.length; i3++)
{
if (greystoneLogos[i3] !== currentLogo)
{
xpoint = randomRange(this.stage.stageWidth - (currentLogo.width * 4.8));
ypoint = randomRange(this.stage.stageHeight - (currentLogo.height * 6.9));
logoBoundaries = getOffDimensions(currentLogo)
for (i4=0; i4 < logoBoundaries.length; i4++)
{
XandY = logoBoundaries[i4].split(":")
while ((xpoint <= (Number(XandY[0]) + Number(currentLogo.width * 4.8)) && xpoint >= (Number(XandY[0]) - Number(currentLogo.width * 4.8))) && (ypoint <= (Number(XandY[1]) + Number(currentLogo.height * 6.9)) && ypoint >= (Number(XandY[1]) - Number(currentLogo.height * 6.9)))){
xpoint = randomRange(this.stage.stageWidth - (currentLogo.width * 4.8));
trace(XandY[0] + " And " + (Number(currentLogo.width * 4.8)))
trace(xpoint + " And " + (Number(XandY[0]) + Number(currentLogo.width * 4.8)))
ypoint = randomRange(this.stage.stageHeight - (currentLogo.height * 6.9));
}
}
}
else
{
continue;
}
}
previousLogos.push(currentLogo);
currentLogo.x = xpoint;
currentLogo.y = ypoint;
stage.addChild(currentLogo);
currentLogo.gotoAndStop(1);
var countTime:Timer = new Timer(waitTime,1);
countTime.addEventListener(TimerEvent.TIMER, function(){
currentLogo.gotoAndPlay(1);
currentLogo.addFrameScript ( currentLogo.totalFrames - 1 , function(){
currentLogo.stop()
stage.removeChild(currentLogo)
if(stage.numChildren <= 1){
getNewSymbols();
}
}) ;
});
countTime.start();
}
function getOffDimensions(currentLogo:MovieClip){
var i3:int;
var tempArr:Array = new Array()
for (i3=0; i3 < greystoneLogos.length; i3++)
{
if (greystoneLogos[i3] !== currentLogo){
tempArr[i3]=greystoneLogos[i3].x +":"+ greystoneLogos[i3].y
}
}
return tempArr
}
function randomRange(max:Number, min:Number = 0):Number
{
return Math.random() * (max - min) + min;
}
There are also may be a handful of unused variables from multiple things I've been trying out.
The code that I posted, will make the movie clip appear at a random spot based on the last movie clip that came up. So let's say we have 3 movie clips (the user will be able to change how many of the clips get displayed) 1 appears at 0,0 the other at 400,400 and the last one appears at 10,10 because I have no way of saving the previous values to compare in the while loop.
I hope this clarifies it a tad more
EDIT:
Based on a function shown below, I've added this:
for (i3=0; i3 < greystoneLogos.length; i3++)
{
if (greystoneLogos[i3] !== currentLogo && greystoneLogos[i3] != null)
{
while(currentLogo.hitTestObject(greystoneLogos[i3]) == true){
xpoint = randomRange(this.stage.stageWidth - (currentLogo.width));
ypoint = randomRange(this.stage.stageHeight - (currentLogo.height));
i3 = 0
}
}else{
continue;
}
}
Which results in a rather bad loop as well as the logo's still overlap above each other
The quickest way I can think to solve this is to do a hit test (http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/DisplayObject.html#hitTestObject()) on the new MovieClip and if it returns true, run the placement code again
Ok, here's the implementation of this quickest way.
You can track your present displayObject by the .numChildren property. You don't even need an array of your logos or things. Let's say you have a function that adds new movieClip to your screen. It knows how many clips you can have (MovieClipDummy is just my testing class - it draws a circle with a specified radius. It should be replaced by your objects).
private function addAnotherMovieClip(): void {
if (_currentDummmiesCount < _dummmiesCount) {
var newDummy: MovieClipDummy = new MovieClipDummy(90);
addChild(newDummy);
//If we get a stackoverflow error (see below)
//we just remove our object from the screen as it will most likely just won't fit
if (checkForEmptySpace(newDummy) != "") {
removeChild(newDummy);
return;
}
} else {
//Do nothing or do something
}
}
It calls another function, checkForEmptySpace, which tries to place your movieclip so it won't overlap with other objects. Here it is:
private function checkForEmptySpace(newClip: Sprite): String {
//you should store your screen width somewhere.
//you can also call stage.stageWidth instead, but first make sure that you
//always have a link to the stage or it will throw an error
newClip.x = Math.random() * _screenWidth;
newClip.y = Math.random() * _screenHeight;
//===========================
//Important part - we try to check all the present children of our container
//numChildren is a property of the DisplayObjectContainer
for (var i: int; i < numChildren; i++) {
//We need a try here because we will get a StackOverflow error if there's no empty space left
try {
//We need to check if our current display object, received with getChildAt()
//is not the same as the one we've just added to the screen
//And if our new object intersects with ANY other object on the stage - we
//call THIS function once again.
//We do recursion because we can easily catch an error and remove this object from
//the screen
if (newClip != getChildAt(i) && newClip.hitTestObject(getChildAt(i))) {
//If our recursive function returns an error - we should pass it further
if (checkForEmptySpace(newClip) != "") {
return "error";
}
}
//The only error that can go here is stackoverflow error. So when we get one
//we return this error string
} catch (error: Error) {
trace(error);
return "error";
}
}
//We only return this empty string if we don't have stackoverflow error
//so there's possibly no space left for another movieclip
return "";
}
You can do it without recursion, but you will have to check for empty space with another logic.
And, you can do it more "professionally" by using Minkowski addition. You should consider storing an array of "boundary" points of your movieclips (let's say every movieclip is a rectangle) and when you add a new object to the screen, you calculate this Minkowsky addition. It will have some "free" spots on your screen which represent any possible coordinates of your new movieclip. It's pretty interesting to implement something like that because the accuracy will be phenomenal. But if you don't have time - just use that recursive placement function

AS3 Pass Object

I have 3 Classes.
One is a Global class that has my function:
public static function getDistance(ObjOne, ObjTwo)
{
var distance = Math.sqrt( ( ObjOne.x - ObjTwo.x ) * ( ObjOne.x - ObjTwo.x ) + ( ObjOne.y - ObjTwo.y ) * ( ObjOne.y - ObjTwo.y ) );
return distance;
}
Then I have a MovieClip that is the class: Minion and another called: Turret
In the Minion class I am calling: Global.getDistance
And setting the args to: Global.getDistance(this, ?????)
How can I get the Turret Object from the Turret class for the final param?
If each minion has a single turret that it is targeting, then you should hold a reference to the turret inside of your Minion class. There should be no need for a static function to get distance (unless it's used for other things besides the minion/turret relation).
For your turrets to be aware of ALL minions (to decide which minion to attack), a good way to do this is to store them all in a statically assessable vector (array).
Here would be a sample of your Minion class:
public class Minion {
public static var allMinions:Vector<Minion> = new Vector<Minion>(); //create a static array to store all your minions
public var turret:Turret;
public function Minion(targetTurret:Turret):void {
turret = targetTurret;
this.addEventListener(Event.ADDED_TO_STAGE,addedToStage,false,0,true);
this.removeEventListener(Event.REMOVED_FROM_STAGE,removedFromStage,false,0,true);
}
private function addedToStage(e:Event):void {
allMinions.push(this); //add this minion instance to the array when it's added to the display list
}
private function removedFromStage(e:Event):void {
//remove this minion instance from the array of all minions when it's removed from the display list
var index:int = allMinions.indexOf(this);
if(index >= 0){
allMinions.splice(index,1);
}
}
private function move():void { //whatever function you use to move the minion along
//get distance here, using your local turret var
getDistance(this,turret); //this could be a local function, or some static function somewhere - local is faster, especially if being called everyframe or in a loop.
}
//a function to take damage, returns true if still alive after the damage
public function takeDamage(amount:Number):Boolean {
this.health -= amount * armor; //armor could be a value between 1 (no armor) and 0 (invincible)
if(health <= 0){
//die
return false;
}
return true;
}
}
This passes in the appropriate turret when you create a new Minion - new Minion(turretInstance), and has a statically accessible array that holds all the minions that are on the display list at any given time. It also adds a function to take damage
For the turrets to attack, you'd want to scan the array of all minions, and determine which are close enough to attack, either attack all (if that's the type of turret) or pick one (the closest one usually) to attack.
Turret class:
public var range:Number = 200; //how close to the turret a minion needs to be before it can attack
public var attackPower:Number = 10; //how much damage does this turret cause
public function attack():void {
//this for loop goes through all the minions and finds the closest one
var closestMinion:Minion;
var tmpDistance:Number; //
var distance:Number;
for each(var minion:Minion in Minion.allMinions){
distance = Math.sqrt( ( minion.x - this.x ) * ( minion.x - this.x ) + ( minion.y - this.y ) * ( minion.y - this.y ) ); //find the distance between the current minion in the loop and this turret
if(distance < range && isNaN(tmpDistance) || distance < tmpDistance){ //if the distance of this minion is less than the current closest, make the current closest this one
closestMinion = minion;
tmpDistance = distance;
}
}
//attack the closest one
if(closestMinion){
closestMinion.takeDamage(attackPower);
}
}
If there is only one Turret in your game, you could use the singleton design pattern.
private static var instance:Turret = null;
public static function getInstance()
{
if(instance === NULL)
instance = new Turret();
return instance;
}
So you can call Turret.getInstance() and use your Turret object. Hope it helps. If you have more than one turret you should have a game class with an array or vector with all your turrets.
I have no idea what you exactly need, the type of the second parameter? Since your dealing with a global function, i would suggest to just use DisplayObject since both have x/y properties
public static function getDistance(clip1:DisplayObject, clip2:DisplayObject):Number
{
return Math.sqrt( ( clip1.x - clip2.x ) * ( clip1.x - clip2.x ) + ( clip1.y - clip2.y ) * ( clip1.y - clip2.y ) );
}

Coords interpolation

we are sending from server to client the coords of the ball each 300 ms. We have to interpolate coords to make the moving smooth. Here is the code (AS3):
private function run(event:Event):void
{
// Current frame ball position
var currentPosition:Point = new Point(this.x, this.y);
// Vector of the speed
_velocity = _destinationPoint.subtract(currentPosition);
// Interpolation
// Game.timeLapse - time from last package with coordinates (last change of destinationPoint)
// stage.frameRate - fps
_velocity.normalize(_velocity.length * 1000 / Game.timeLapse / stage.frameRate);
// If ball isn't at the end of the path, move it
if (Point.distance(currentPosition, _destinationPoint) > 1) {
this.x += _velocity.x;
this.y += _velocity.y;
} else {
// Otherwise (we are at the end of the path - remove listener from this event
this.removeEventListener(Event.ENTER_FRAME, run);
this.dispatchEvent(new GameEvent(GameEvent.PLAYER_STOP));
}
}
The problem is described in the following picture:
Red point - destination point
Black lines - lines from curret point to destination without
normalization
Green dotted - the path of the ball
Maybe there is a way to make moving smooth but more accurate?
If you want to interpolate path steps for exactly three points, you need to use quadratic Bezier curve math to be able to calculate any position on the curve for any given distance from its starting point. You need this to get equal steps along the curve, that you have on your picture. That's rather tricky, because when you use bezier curve equiations in polynomial form, you don't get equal distance along the curve for equal parameter deltas.So, you need to treat bezier curve as a parabola segment (which it effectively is), and the task can be reformulated as "stepping along a parabolic curve with steps of equal length". This is still quite tricky, but fortunately there is a solution out there: http://code.google.com/p/bezier/
I used this library several times (to make equal steps along a parabolic curve) and it worked perfectly well for me.
Most likely you would want to interpolate between arbitrary set of points. If this is the case, you may use Lagrange approximation.Below is my simple implementation of Lagrange approximation. (Googling for it will certainly give you more.) You supply approximator with arbitrary number of known function values and it can generate the value of a smooth function for any value of the argument in between.
--
package org.noregret.math
{
import flash.geom.Point;
import flash.utils.Dictionary;
/**
* #author Michael "Nox Noctis" Antipin
*/
public class LagrangeApproximator {
private const points:Vector.<Point> = new Vector.<Point>();
private const pointByArg:Dictionary = new Dictionary();
private var isSorted:Boolean;
public function LagrangeApproximator()
{
}
public function addValue(argument:Number, value:Number):void
{
var point:Point;
if (pointByArg[argument] != null) {
trace("LagrangeApproximator.addValue("+arguments+"): ERROR duplicate function argument!");
point = pointByArg[argument];
} else {
point = new Point();
points.push(point);
pointByArg[argument] = point;
}
point.x = argument;
point.y = value;
isSorted = false;
}
public function getApproximationValue(argument:Number):Number
{
if (!isSorted) {
isSorted = true;
points.sort(sortByArgument);
}
var listLength:uint = points.length;
var point1:Point, point2:Point;
var result:Number = 0;
var coefficient:Number;
for(var i:uint =0; i<listLength; i++) {
coefficient = 1;
point1 = points[i];
for(var j:uint = 0; j<listLength; j++) {
if (i != j) {
point2 = points[j];
coefficient *= (argument-point2.x) / (point1.x-point2.x);
}
}
result += point1.y * coefficient;
}
return result;
}
private function sortByArgument(a:Point, b:Point):int
{
if (a.x < b.x) {
return -1;
}
if (a.x > b.x) {
return 1;
}
return 0;
}
public function get length():int
{
return points.length;
}
public function clear():void
{
points.length = 0;
var key:*;
for (key in pointByArg) {
delete pointByArg[key];
}
}
}
}
You could send more than one coordinate each tick. Or send some extra properties along with each point, maybe to say if it is a point where the ball bounces, or if it can be smoothed.
Sending a series of points in one transaction would give you greater accuracy, and wont add too much to the packet size, compared to the overhead of sending, processing, and receiving.

What is the most effective way to test for combined keyboard arrow direction in ActionScript 3.0?

I need to monitor the direction a user is indicating using the four directional arrow keys on a keyboard in ActionScript 3.0 and I want to know the most efficient and effective way to do this.
I've got several ideas of how to do it, and I'm not sure which would be best. I've found that when tracking Keyboard.KEY_DOWN events, the event repeats as long as the key is down, so the event function is repeated as well. This broke the method I had originally chosen to use, and the methods I've been able to think of require a lot of comparison operators.
The best way I've been able to think of would be to use bitwise operators on a uint variable. Here's what I'm thinking
var _direction:uint = 0x0; // The Current Direction
That's the current direction variable. In the Keyboard.KEY_DOWN event handler I'll have it check what key is down, and use a bitwise AND operation to see if it's already toggled on, and if it's not, I'll add it in using basic addition. So, up would be 0x1 and down would be 0x2 and both up and down would be 0x3, for example. It would look something like this:
private function keyDownHandler(e:KeyboardEvent):void
{
switch(e.keyCode)
{
case Keyboard.UP:
if(!(_direction & 0x1)) _direction += 0x1;
break;
case Keyboard.DOWN:
if(!(_direction & 0x2)) _direction += 0x2;
break;
// And So On...
}
}
The keyUpHandler wouldn't need the if operation since it only triggers once when the key goes up, instead of repeating. I'll be able to test the current direction by using a switch statement labeled with numbers from 0 to 15 for the sixteen possible combinations. That should work, but it doesn't seem terribly elegant to me, given all of the if statements in the repeating keyDown event handler, and the huge switch.
private function checkDirection():void
{
switch(_direction)
{
case 0:
// Center
break;
case 1:
// Up
break;
case 2:
// Down
break;
case 3:
// Up and Down
break;
case 4:
// Left
break;
// And So On...
}
}
Is there a better way to do this?
You can keep track of whether each key is down or not by listening for all KEY_DOWN and KEY_UP events, and storing each key state in an array. I wrote a class a while ago to do just that (included at the end of my answer).
Then you are no longer tied to the event model to know if a certain key is down or not; you can periodically check every frame (or every timer interval). So you could have a function like:
function enterFrameCallback(e:Event):void
{
var speed:Number = 1.0; // net pixels per frame movement
thing.x += (
-(int)Input.isKeyDown(Keyboard.LEFT)
+(int)Input.isKeyDown(Keyboard.RIGHT)
) * speed;
thing.y += (
-(int)Input.isKeyDown(Keyboard.UP)
+(int)Input.isKeyDown(Keyboard.DOWN)
) * speed;
}
which would take into account all possible combinations of arrow key presses. If you want the net displacement to be constant (e.g. when going right and down at same time, the object moves X pixels diagonally, as opposed to X pixels in both horizontal and vertical directions), the code becomes:
function enterFrameCallback(e:Event):void
{
var speed:Number = 1.0; // net pixels per frame movement
var displacement:Point = new Point();
displacement.x = (
-(int)Input.isKeyDown(Keyboard.LEFT)
+(int)Input.isKeyDown(Keyboard.RIGHT)
);
displacement.y = (
-(int)Input.isKeyDown(Keyboard.UP)
+(int)Input.isKeyDown(Keyboard.DOWN)
);
displacement.normalize(speed);
thing.x += displacement.x;
thing.y += displacement.y;
}
Here is the Input class I wrote (don't forget to call init from the document class). Note that it also keeps track of mouse stuff; you can delete that if you don't need it:
/*******************************************************************************
* DESCRIPTION: Defines a simple input class that allows the programmer to
* determine at any instant whether a specific key is down or not,
* or if the mouse button is down or not (and where the cursor
* is respective to a certain DisplayObject).
* USAGE: Call init once before using any other methods, and pass a reference to
* the stage. Use the public methods commented below to query input states.
*******************************************************************************/
package
{
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.display.Stage;
import flash.geom.Point;
import flash.display.DisplayObject;
public class Input
{
private static var keyState:Array = new Array();
private static var _mouseDown:Boolean = false;
private static var mouseLoc:Point = new Point();
private static var mouseDownLoc:Point = new Point();
// Call before any other functions in this class:
public static function init(stage:Stage):void
{
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown, false, 10);
stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp, false, 10);
stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown, false, 10);
stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp, false, 10);
stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove, false, 10);
}
// Call to query whether a certain keyboard key is down.
// For a non-printable key: Input.isKeyDown(Keyboard.KEY)
// For a letter (case insensitive): Input.isKeyDown('A')
public static function isKeyDown(key:*):Boolean
{
if (typeof key == "string") {
key = key.toUpperCase().charCodeAt(0);
}
return keyState[key];
}
// Property that is true if the mouse is down, false otherwise:
public static function get mouseDown():Boolean
{
return _mouseDown;
}
// Gets the current coordinates of the mouse with respect to a certain DisplayObject.
// Leaving out the DisplayObject paramter will return the mouse location with respect
// to the stage (global coordinates):
public static function getMouseLoc(respectiveTo:DisplayObject = null):Point
{
if (respectiveTo == null) {
return mouseLoc.clone();
}
return respectiveTo.globalToLocal(mouseLoc);
}
// Gets the coordinates where the mouse was when it was last down or up, with respect
// to a certain DisplayObject. Leaving out the DisplayObject paramter will return the
// location with respect to the stage (global coordinates):
public static function getMouseDownLoc(respectiveTo:DisplayObject = null):Point
{
if (respectiveTo == null) {
return mouseDownLoc.clone();
}
return respectiveTo.globalToLocal(mouseDownLoc);
}
// Resets the state of the keyboard and mouse:
public static function reset():void
{
for (var i:String in keyState) {
keyState[i] = false;
}
_mouseDown = false;
mouseLoc = new Point();
mouseDownLoc = new Point();
}
///// PRIVATE METHODS BEWLOW /////
private static function onMouseDown(e:MouseEvent):void
{
_mouseDown = true;
mouseDownLoc = new Point(e.stageX, e.stageY);
}
private static function onMouseUp(e:MouseEvent):void
{
_mouseDown = false;
mouseDownLoc = new Point(e.stageX, e.stageY);
}
private static function onMouseMove(e:MouseEvent):void
{
mouseLoc = new Point(e.stageX, e.stageY);
}
private static function onKeyDown(e:KeyboardEvent):void
{
keyState[e.keyCode] = true;
}
private static function onKeyUp(e:KeyboardEvent):void
{
keyState[e.keyCode] = false;
}
}
}