Is there built-in AI in TGB? And if so...
by Chris Jorgensen · in Torque Game Builder · 09/12/2006 (12:49 pm) · 27 replies
How would I use it?
If not, has anyone out there done AI in script? And how did you go about doing it?
I did a simple one which, while pretty stupid, has done the trick for testing purposes. I basically created a script object and assigned it to a ship. Then it would call the same functions the human player controls call based on if it decision. But I don't like how I wrote it. Basically, every half second it would look at itself, its surroundings, and its experiences (just some feedback controlled decision thresholds), and decide if it wanted to attack, evade, or search.
It's problems:
1- too slow at making decisions
2- hard to balance ratio of attack/evade
3- does a poor job aiming at other ship (fires at where its target is, not where it will be)
Anyway, just curious if others have tried this, or if it's insane to to AI in script.
If not, has anyone out there done AI in script? And how did you go about doing it?
I did a simple one which, while pretty stupid, has done the trick for testing purposes. I basically created a script object and assigned it to a ship. Then it would call the same functions the human player controls call based on if it decision. But I don't like how I wrote it. Basically, every half second it would look at itself, its surroundings, and its experiences (just some feedback controlled decision thresholds), and decide if it wanted to attack, evade, or search.
It's problems:
1- too slow at making decisions
2- hard to balance ratio of attack/evade
3- does a poor job aiming at other ship (fires at where its target is, not where it will be)
Anyway, just curious if others have tried this, or if it's insane to to AI in script.
About the author
Owner of Cascadia Games LLC
#2
If you have TGE, Chris, there's a good script based AI resource, Killer Kork, that would be beneficial to get and see how its implemented.
Converting to TGB, meaning the concepts, shouldn't be too bad.
09/12/2006 (3:39 pm)
The adventure kit is a great add-on. i'm trying to allocate funds to get it.If you have TGE, Chris, there's a good script based AI resource, Killer Kork, that would be beneficial to get and see how its implemented.
Converting to TGB, meaning the concepts, shouldn't be too bad.
#3
Oh, you did it in script? For some reason I assumed you'd changed the source itself. Well that's interesting. Did you have it aim more accurately? That is, where the target will be and not where it is at the moment it decides to shoot? I think if I add that in and tune it, my present AI might be good for an easy AI setting.
I'd really love to write a killer AI though. I tried a simple GA but it didnt' work too hot. I think evolving the actual actions themselves (that is, do random things until you evolve a set of coherent actions) wasn't smart for script, given how slow it is called and how hard it is to judge fitness of an action. I mean, if in some time window, you don't get hurt and the other guy doesn't get hurt, is that a good or bad action? Plus, in a 30 second game, we'll say, the AI needs to learn quick to keep up with a human. So that was a poor choice, imo!
09/12/2006 (3:42 pm)
@TomOh, you did it in script? For some reason I assumed you'd changed the source itself. Well that's interesting. Did you have it aim more accurately? That is, where the target will be and not where it is at the moment it decides to shoot? I think if I add that in and tune it, my present AI might be good for an easy AI setting.
I'd really love to write a killer AI though. I tried a simple GA but it didnt' work too hot. I think evolving the actual actions themselves (that is, do random things until you evolve a set of coherent actions) wasn't smart for script, given how slow it is called and how hard it is to judge fitness of an action. I mean, if in some time window, you don't get hurt and the other guy doesn't get hurt, is that a good or bad action? Plus, in a 30 second game, we'll say, the AI needs to learn quick to keep up with a human. So that was a poor choice, imo!
#4
From what I read from your post, it seems that you think that I had something to do with the resource.
I did not.
I just posted a resource that I thought would be good for you to look at.
09/12/2006 (3:48 pm)
Oops, I think there is some confusion.From what I read from your post, it seems that you think that I had something to do with the resource.
I did not.
I just posted a resource that I thought would be good for you to look at.
#5
Fix your aiming and then invest some time on thinking out the different behaviors and when you should switch between them. You can do an awful lot with really simple techniques... especially in 2D.
09/12/2006 (3:50 pm)
I've not been a fan of GA driven AI opponents. I've not seen any real world examples where it's easier to develop or smarter than a good old state machine driven system.Fix your aiming and then invest some time on thinking out the different behaviors and when you should switch between them. You can do an awful lot with really simple techniques... especially in 2D.
#6
Well, I definitely think going the route of evolving behaviors is wrong. A different technique might be to do an FSM, but tune things like the arcs between states, how long to stay in a state, etc, via a GA. I have a bit of a bias to trying one out, since my thesis research right now uses GAs. (Admittedly, it's not going as smooth as I'd thought it would.) Or I could just put those thresholds into a gene and use... dang, I forget the name, but you basically emphasize mutation instead of recombination, and slowly shrink the mutations. The theory is you just sort of naturally tune to the ideal settings. But, ya, decent aiming takes precedence.
@David
I think I just posted before seeing your post. It's been edited. I don't have TGE unfortunately. While admittedly I could just go out and buy it, i'm hesitant to let my tiny game's budget grow. If it makes back the ~$500 budget it's taking (not counting hours of free labor), I'll be surprised and thrilled.
09/12/2006 (4:13 pm)
@TomWell, I definitely think going the route of evolving behaviors is wrong. A different technique might be to do an FSM, but tune things like the arcs between states, how long to stay in a state, etc, via a GA. I have a bit of a bias to trying one out, since my thesis research right now uses GAs. (Admittedly, it's not going as smooth as I'd thought it would.) Or I could just put those thresholds into a gene and use... dang, I forget the name, but you basically emphasize mutation instead of recombination, and slowly shrink the mutations. The theory is you just sort of naturally tune to the ideal settings. But, ya, decent aiming takes precedence.
@David
I think I just posted before seeing your post. It's been edited. I don't have TGE unfortunately. While admittedly I could just go out and buy it, i'm hesitant to let my tiny game's budget grow. If it makes back the ~$500 budget it's taking (not counting hours of free labor), I'll be surprised and thrilled.
#7
Do you have TGB Pro? Perhaps a mix of code and script could provide the performance/features.
09/12/2006 (4:24 pm)
I wouldn't say that it's worth $600 (500 already spent + 100 to get TGE). (Though to get TGE right now is better valued than it will be.)Do you have TGB Pro? Perhaps a mix of code and script could provide the performance/features.
#8
That's my 2 cents.
Also: I recently wrote a FSM class in TS that I'm using for animation control and AI. If anyone is interested, I would be more than happy to share it.
-Thomas
09/13/2006 (4:21 am)
This is a really interesting topic. First off, to answer the original question, there is unfortunately no built-in AI infrastructure in TGB. That said, I feel like I should step in and mention that it's really easy to overdevelop AI. If you put a lot into your AI to give your unmanned characters more versatility, you may find that their actions seem unnatural and take away from the experience (while costing more resources). Now I'm not saying fun is neccesarily easy, but a slight randomization and a solid state system is more than enough for most games' needs.That's my 2 cents.
Also: I recently wrote a FSM class in TS that I'm using for animation control and AI. If anyone is interested, I would be more than happy to share it.
-Thomas
#9
09/13/2006 (6:41 am)
Thomas, that would be fantastic. AI fascinates me even as a lot of the nitty gritty escapes me, and I'd love to take a peek at what you've done. :)
#11
I am already using a pretty decent class posted by someone here on the forums (modified for my own desires), it might even be yours if you had already posted something like this earlier.
Gracias.
09/13/2006 (9:20 am)
Thomas, I would certainly be interested in this code.I am already using a pretty decent class posted by someone here on the forums (modified for my own desires), it might even be yours if you had already posted something like this earlier.
Gracias.
#12
I'd be curious as well. I agree, I can see how quick one can fall down the slipperly slope of overdeveloping an AI. I wrote this EA-API (in C) for my thesis that I've just been itching to try it out for AI. But recently I've skewd more toward trying to do the whole game in script. I probably should just write a little state machine as everone suggests, and worry about the "killer" AI down the road if I need it.
Well it's too bad there isn't built-in AI. Simpler is always better in my mind. Right now I just have a "brain" script object that I attach to the spaceship that I want it to control. My theory was it'd be easy to swap in and out AI's for testing, as well as see which AI dominates which. I'm ready to start plugging in a few. :)
09/13/2006 (11:39 am)
@ ThomasI'd be curious as well. I agree, I can see how quick one can fall down the slipperly slope of overdeveloping an AI. I wrote this EA-API (in C) for my thesis that I've just been itching to try it out for AI. But recently I've skewd more toward trying to do the whole game in script. I probably should just write a little state machine as everone suggests, and worry about the "killer" AI down the road if I need it.
Well it's too bad there isn't built-in AI. Simpler is always better in my mind. Right now I just have a "brain" script object that I attach to the spaceship that I want it to control. My theory was it'd be easy to swap in and out AI's for testing, as well as see which AI dominates which. I'm ready to start plugging in a few. :)
#13
Building in AI into an engine like TGB would be one of the most complex and difficult systems possible--what game genre type should the AI be able to include? Poker? schmups? side scrollers? puzzlers?
The system would have to be so generic (or conversely, detailed and cumbersome) to meet everyone's needs that it wouldn't be worth developing in my personal opinion.
09/13/2006 (12:49 pm)
Not trying to lecture, but think about what you are asking: a "simple" "built in AI". Building in AI into an engine like TGB would be one of the most complex and difficult systems possible--what game genre type should the AI be able to include? Poker? schmups? side scrollers? puzzlers?
The system would have to be so generic (or conversely, detailed and cumbersome) to meet everyone's needs that it wouldn't be worth developing in my personal opinion.
#14
As far as TGB having an AI, I figured there wasn't anything. But I thought I'd ask. Things like tracking/evading/etc are simple enough, so I guess I just wondered if maybe things along those lines existed. Sort of like, why should I call pickradius/check the ids/choose an action, etc, if there was just some sort of "track" call in there -- not so much a complete AI as the building blocks for one. (Not that it isn't simple enough to write these things as is. I was just curious.)
Though it's just bad writing when I said "Well it's too bad there isn't built-in AI. Simpler is always better in my mind." Those were separate thoughts. The "too bad" bit was just meant as a closer to that topic. You know, like "oh well, can't blame me for asking." The "simpler" AI here was just my response to Thomas's note that AIs can get overly complex. I really should have split those apart. I can see how it would imply I think AI in the engine is simple. Not intended to mean that. Blame that on poor grammar. :)
09/13/2006 (2:08 pm)
@StephenAs far as TGB having an AI, I figured there wasn't anything. But I thought I'd ask. Things like tracking/evading/etc are simple enough, so I guess I just wondered if maybe things along those lines existed. Sort of like, why should I call pickradius/check the ids/choose an action, etc, if there was just some sort of "track" call in there -- not so much a complete AI as the building blocks for one. (Not that it isn't simple enough to write these things as is. I was just curious.)
Though it's just bad writing when I said "Well it's too bad there isn't built-in AI. Simpler is always better in my mind." Those were separate thoughts. The "too bad" bit was just meant as a closer to that topic. You know, like "oh well, can't blame me for asking." The "simpler" AI here was just my response to Thomas's note that AIs can get overly complex. I really should have split those apart. I can see how it would imply I think AI in the engine is simple. Not intended to mean that. Blame that on poor grammar. :)
#15
One of which that gets talked about often is some foundational script classes for the very things you mention.
09/13/2006 (2:15 pm)
On a lighter side, GG has played around with several ideas (Joe M's team for mini-tutorials specifically, although I don't want to steal his tunder) for providing quite a few types of "modules" that can be pieced together as developers like for their game functionality.One of which that gets talked about often is some foundational script classes for the very things you mention.
#16
I'll try to remember to post up the FSM class tomorrow when I get into the office. I have an older version of it with me, but I remember fixing something in it recently and I want to make sure you guys get the right version. Maybe I'll even toss it up on TDN with some examples.
Edit: Added winking smileys where appropriate. ;)
09/14/2006 (3:03 am)
'Team Joe' rules! ;)I'll try to remember to post up the FSM class tomorrow when I get into the office. I have an older version of it with me, but I remember fixing something in it recently and I want to make sure you guys get the right version. Maybe I'll even toss it up on TDN with some examples.
Edit: Added winking smileys where appropriate. ;)
#17
t2dFSM.cs
... and here is a simple example FSM using this system. It switches between states based on keyboard input.
FSMTest.cs
That should get you started. Enjoy, and let me know if you have any trouble with it.
-Thomas
09/15/2006 (2:05 pm)
Ok guys, sorry about the delay. It totally slipped my mind yesterday, but here it is. Just save the following as t2dFSM.cs and make sure you exec it in your project. The t2dFSM class is intended to be used as a superclass for a custom finite state machine class to make it easier to define states and transitions. If you don't create a filter function for a state, the FSM will not register it. EnterState and ExitState functions are optional and will only be called if they exist. If you have trouble implementing it, let me know. Below t2dFSM.cs I will post a simple example.t2dFSM.cs
//---------------------------------------------------------------------------------------------
// Torque Game Builder
// Copyright (C) GarageGames.com, Inc.
//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
// t2dFSM
// The t2dFSM object is a general finite state machine that can be used for controlling
// animations, artificial intelligence, or any other state based procedure.
//
// Creating a t2dFSM object:
//
// %fsm = new ScriptObject()
// {
// class = "MyFSM";
// superClass = "t2dFSM";
// };
//
// Creating states for the FSM:
//
// %fsm.registerState( "A" );
// %fsm.registerState( "B" );
//
// State transitions:
//
// function MyFSM::filterState_A( %this )
// {
// if( gotoStateB() )
// return "B";
// }
//
// Callbacks are called upon entering and leaving each state, if they exist.
//
// function MyFSM::exitState_A( %this )
// {
// echo( "Exiting State A" );
// }
//
// function MyFSM::enterState_B( %this )
// {
// echo( "Entering State B. );
// }
//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
// t2dFSM::registerState
// Registers a state with the FSM
//---------------------------------------------------------------------------------------------
function t2dFSM::registerState( %this, %newStateName )
{
if( %this.hasState( %newStateName ) )
{
warn( "t2dFSM::registerState - Failed to register state " @ %newStateName @ ". State " @
"already exists." );
return false;
}
if( !%this.isMethod( "filterState_" @ %newStateName ) )
{
warn( "t2dFSM::registerState - Failed to register state " @ %newStateName @ ". Filter " @
"method does not exist for " @ %this.class @ "." );
return false;
}
%this.stateLookup[%newStateName] = "_";
return true;
}
//---------------------------------------------------------------------------------------------
// t2dFSM::hasState
// Returns whether or not a state exists for this FSM.
//---------------------------------------------------------------------------------------------
function t2dFSM::hasState( %this, %stateName )
{
return %this.stateLookup[%stateName] !$= "";
}
//---------------------------------------------------------------------------------------------
// t2dFSM::removeState
// Removes a state from the FSM.
//---------------------------------------------------------------------------------------------
function t2dFSM::removeState( %this, %stateName )
{
if( %this.hasState( %stateName ) )
{
%this.stateLookup[%stateName] = "";
return true;
}
return false;
}
//---------------------------------------------------------------------------------------------
// t2dFSM::forceState
// Forces the FSM into a state without calling the enter and exit methods.
//---------------------------------------------------------------------------------------------
function t2dFSM::forceState( %this, %stateName )
{
%this.currentState = %stateName;
}
//---------------------------------------------------------------------------------------------
// t2dFSM::setState
// Sets the state of the FSM, calling methods indicating the change.
//---------------------------------------------------------------------------------------------
function t2dFSM::setState( %this, %stateName )
{
if( ( %stateName !$= %this.currentState ) && %this.hasState( %stateName ) )
{
if( %this.isMethod( "exitState_" @ %this.currentState ) )
eval( %this @ ".exitState_" @ %this.currentState @ "();" );
%this.prevState = %this.currentState;
%this.currentState = %stateName;
if( %this.isMethod( "enterState_" @ %this.currentState ) )
eval( %this @ ".enterState_" @ %this.currentState @ "();" );
return true;
}
return false;
}
//---------------------------------------------------------------------------------------------
// t2dFSM::checkState
// Calls the filter method for the current state to determine if the state should change.
//---------------------------------------------------------------------------------------------
function t2dFSM::checkState( %this )
{
if( %this.hasState( %this.currentState ) )
{
if( %this.isMethod( "filterState_" @ %this.currentState ) )
{
%nextState = eval( %this @ ".filterState_" @ %this.currentState @ "();" );
if( %nextState !$= "" )
%this.setState( %nextState );
return %nextState;
}
else
{
%this.removeState( %this.currentState );
}
}
return "";
}... and here is a simple example FSM using this system. It switches between states based on keyboard input.
FSMTest.cs
moveMap.bindCmd(keyboard, "a", "$apressed=true;", "$apressed=false;");
moveMap.bindCmd(keyboard, "b", "$bpressed=true;", "$bpressed=false;");
moveMap.bindCmd(keyboard, "c", "$cpressed=true;", "$cpressed=false;");
moveMap.bindCmd(keyboard, "z", "$zpressed=true;", "$zpressed=false;");
function t2dSceneGraph::onUpdateScene(%this)
{
if( isObject( MyFSM ) )
MyFSM.checkState();
}
function TestFSM::enterState_aState(%this)
{
echo("entering A state..");
}
function TestFSM::filterState_aState(%this)
{
if($bPressed)
return "bState";
}
function TestFSM::exitState_aState(%this)
{
echo("leaving A state..");
}
function TestFSM::enterState_bState(%this)
{
echo("entering B state..");
}
function TestFSM::filterState_bState(%this)
{
if($cPressed)
return "cState";
}
function TestFSM::exitState_bState(%this)
{
echo("leaving B state..");
}
function TestFSM::enterState_cState(%this)
{
echo("entering C state..");
}
function TestFSM::filterState_cState(%this)
{
if($aPressed)
return "aState";
}
new ScriptObject(MyFSM)
{
class = "TestFSM";
superclass="t2dFSM";
};
MyFSM.registerState("aState");
MyFSM.registerState("bState");
MyFSM.registerState("cState");
MyFSM.setState("aState");That should get you started. Enjoy, and let me know if you have any trouble with it.
-Thomas
#18
I got the test to work, but when I tried to implement an ultra-simple version of my idea, I had some trouble...
This is your test with different state names and keys. No animations or anything here, but let's say I wanted a character who can either stand there and breathe or walk to the right.
If I press right (or if right is ALREADY PRESSED) in the RstandState, she'll enter the RwalkState.
If I let go of right in the RwalkState (or am simply not touching right), she'll enter the RstandState.
I would've expected
But when I test with the "true" statement, I always get the echo that shows the character is walking.
If I just say "if($rightPressed)" without the "=true", the echo will not show walking until after I pressed right.
Also... I never get an echo showing that the character ever stopped walking.
This may be a good time to admit I have no idea how to get animations to work, in case anyone is wondering why I didn't just add the animations in the state machine and test that way. I completed the platformer tutorial, but apparently, I haven't quite grasped exactly how my animation was loaded.
So, to be sheepishly honest, I really need help in two areas. Anyway, here's my test script below.
The main question is whether my true/false idea is valid, and how to execute it.
Bonus points for anyone who can explain where the getAnimationName & playAnimation lines fit in, and what else I need to make the animation actually work. I'm assuming I should have the player on the map with the class & superclass set like this:
09/26/2006 (10:37 pm)
Thomas, thanks for pointing out this thread...I got the test to work, but when I tried to implement an ultra-simple version of my idea, I had some trouble...
This is your test with different state names and keys. No animations or anything here, but let's say I wanted a character who can either stand there and breathe or walk to the right.
If I press right (or if right is ALREADY PRESSED) in the RstandState, she'll enter the RwalkState.
If I let go of right in the RwalkState (or am simply not touching right), she'll enter the RstandState.
I would've expected
Quote:and
if($rightPressed=true)
return "RwalkState";
Quote:to work.
if($rightPressed=false)
return "RstandState";
But when I test with the "true" statement, I always get the echo that shows the character is walking.
If I just say "if($rightPressed)" without the "=true", the echo will not show walking until after I pressed right.
Also... I never get an echo showing that the character ever stopped walking.
This may be a good time to admit I have no idea how to get animations to work, in case anyone is wondering why I didn't just add the animations in the state machine and test that way. I completed the platformer tutorial, but apparently, I haven't quite grasped exactly how my animation was loaded.
So, to be sheepishly honest, I really need help in two areas. Anyway, here's my test script below.
moveMap.bindCmd(keyboard, "right", "$rightpressed=true;", "$rightpressed=false;");
moveMap.bindCmd(keyboard, "left", "$leftpressed=true;", "$leftpressed=false;");
moveMap.bindCmd(keyboard, "up", "$uppressed=true;", "$uppressed=false;");
moveMap.bindCmd(keyboard, "down", "$downpressed=true;", "$downpressed=false;");
function t2dSceneGraph::onUpdateScene(%this)
{
if( isObject( MyFSM ) )
MyFSM.checkState();
}
function CLplayer::enterState_RstandState(%this)
{
echo("CL is just standing there..");
}
function CLplayer::filterState_RstandState(%this)
{
if($rightPressed=true)
return "RwalkState";
}
function CLplayer::exitState_RstandState(%this)
{
echo("She's on the move..");
}
function CLplayer::enterState_RwalkState(%this)
{
echo("Now, she's walking..");
}
function CLplayer::filterState_RwalkState(%this)
{
if($rightPressed=false)
return "RstandState";
}
function CLplayer::exitState_RwalkState(%this)
{
echo("She stopped..");
}
new ScriptObject(MyFSM)
{
class = "CLplayer";
superclass="t2dFSM";
};
MyFSM.registerState("RstandState");
MyFSM.registerState("RwalkState");
MyFSM.setState("RstandState");The main question is whether my true/false idea is valid, and how to execute it.
Bonus points for anyone who can explain where the getAnimationName & playAnimation lines fit in, and what else I need to make the animation actually work. I'm assuming I should have the player on the map with the class & superclass set like this:
Quote:
class = "CLplayer";
superclass="t2dFSM"
#19
Double equals is a comparison and single equals sign assigns the value. That should fix something at the least.
Hope that gets it back on track!
An example:
The 2nd and 3rd versions are the same, but the third is obviously preferred.
09/26/2006 (10:48 pm)
Well, I didn't read all the code, but in if statements, you (99.9999%) always want to use "==" and not "="Double equals is a comparison and single equals sign assigns the value. That should fix something at the least.
Hope that gets it back on track!
An example:
if($rightPressed=true) //this is going to set %rightPressed to true
return "RwalkState";
if($rightPressed==true) //this is going to return true if $rightPressed is true
return "RwalkState";
if($rightPressed) //this is going to return true if $rightPressed is true
return "RwalkState";The 2nd and 3rd versions are the same, but the third is obviously preferred.
#20
Now my test works!!
Now I'll need to see what I can do to fit in the animations... of course, if anyone can point me in the right direction in that area, I'll be in very good shape!
EDIT- By the way, any difference at all between 2 & 3?
Ex:
if(...Pressed==true) - if already holding the button when the state was entered (before entry)
if(...Pressed) - if pressed while in the current state (after entry)
That would make a difference, since in an attack or jump state, buttons don't respond, but... if the player is already holding a direction, he expects to continue walking after the jump/attack. On the other hand, I don't want the player to continue jumping or attacking by holding down the button.
Apologies for my newbieness. If I knew how, I would just add the animations, and if I wasn't 17 days from my wedding, I would spend more time trying to understand why my animation worked in the platformer tutorial.
09/26/2006 (11:20 pm)
Ah... not one, but two equals signs! Thanks, Tom. There's the proof that I am a programming novice.Now my test works!!
Now I'll need to see what I can do to fit in the animations... of course, if anyone can point me in the right direction in that area, I'll be in very good shape!
EDIT- By the way, any difference at all between 2 & 3?
Ex:
if(...Pressed==true) - if already holding the button when the state was entered (before entry)
if(...Pressed) - if pressed while in the current state (after entry)
That would make a difference, since in an attack or jump state, buttons don't respond, but... if the player is already holding a direction, he expects to continue walking after the jump/attack. On the other hand, I don't want the player to continue jumping or attacking by holding down the button.
Apologies for my newbieness. If I knew how, I would just add the animations, and if I wasn't 17 days from my wedding, I would spend more time trying to understand why my animation worked in the platformer tutorial.
Associate Tom Spilman
Sickhead Games
We use scheduled() think calls scaling the think rate based on distance to the camera. Each think call it evaluates it's current goal and decides if a new goal needs to be set. When it does aim it considers the velocity of the target and roughly predicts the position.
Nothing here needs to be exact as AI is expected to not be perfect in most cases. You want them to not aim perfectly and you want them to react within reasonable human rates.
We found that it was easily achievable via script. Now if you have some hardcore path finding to do then *maybe* you need that in C++, but the logic that glues it all together doesn't have to be.