Making StarterGame 2 player?
by Ty Newton · in Torque X 2D · 01/14/2007 (9:33 pm) · 26 replies
Hi,
I'm modifying the StarterGame sample to handle 2 player input using the one keyboard: player1 = WASD, player2 = arrow keys.
I have 2 sprites in the level, both have the MovementComponent assigned to them.
When I mess around with the MovementComponent code I can't get anything to work properly. It seems like only 1 MovementComponent object is instantiated; rather than 1 for each sprite. Is this correct?
So I tried coding MovementComponent as if I was doing a fork() in C. This didn't work either.
Do I need to create a totally new component to make this work e.g. MovementComponentForPlayer2?
I tried looking at the tankbuster (I think ? I'm not at my dev machine atm) example and it is coded totally differently to StarterGame so I couldn't work out what was going on there.
Any advice?
Thanks,
T
I'm modifying the StarterGame sample to handle 2 player input using the one keyboard: player1 = WASD, player2 = arrow keys.
I have 2 sprites in the level, both have the MovementComponent assigned to them.
When I mess around with the MovementComponent code I can't get anything to work properly. It seems like only 1 MovementComponent object is instantiated; rather than 1 for each sprite. Is this correct?
So I tried coding MovementComponent as if I was doing a fork() in C. This didn't work either.
Do I need to create a totally new component to make this work e.g. MovementComponentForPlayer2?
I tried looking at the tankbuster (I think ? I'm not at my dev machine atm) example and it is coded totally differently to StarterGame so I couldn't work out what was going on there.
Any advice?
Thanks,
T
#2
(Note that the playerIndex corresponds to the player number in the editor...)
_SetupInputMap() in MovementComponent.cs (starting from line 79)
01/16/2007 (4:49 am)
Try this in _SetupInputMap.. it gives player 0 the arrow keys and player 1 WASD:(Note that the playerIndex corresponds to the player number in the editor...)
_SetupInputMap() in MovementComponent.cs (starting from line 79)
// keyboard controls
int keyboardId = InputManager.Instance.FindDevice(keyboard);
if (keyboardId >= 0)
{
switch (playerIndex)
{
case 0:
inputMap.BindMove(keyboardId, (int)Keys.Right, MoveMapTypes.StickDigitalRight, 0);
inputMap.BindMove(keyboardId, (int)Keys.Left, MoveMapTypes.StickDigitalLeft, 0);
inputMap.BindMove(keyboardId, (int)Keys.Up, MoveMapTypes.StickDigitalUp, 0);
inputMap.BindMove(keyboardId, (int)Keys.Down, MoveMapTypes.StickDigitalDown, 0);
break;
case 1:
inputMap.BindMove(keyboardId, (int)Keys.D, MoveMapTypes.StickDigitalRight, 0);
inputMap.BindMove(keyboardId, (int)Keys.A, MoveMapTypes.StickDigitalLeft, 0);
inputMap.BindMove(keyboardId, (int)Keys.W, MoveMapTypes.StickDigitalUp, 0);
inputMap.BindMove(keyboardId, (int)Keys.S, MoveMapTypes.StickDigitalDown, 0);
break;
}
}
#3
There is also a callback method in the class that give the player number. This is hard coded to 0. I changed it so it reflected either 0 or 1 depending on the _owner's name.
I should get a chance to play with this stuff again in a day or two. I'll post my findings...
It's cherry season here in Australia and I've been busy preserving cherries in my spare time. I love a boozy, thick cherry sauce...
01/16/2007 (6:43 pm)
I havn't had a chance to mess around with this again. But what you (Thomas) suggested is the way I originally tried it. The result didn't seem to work; case 1 was the only bit of code that was operating.There is also a callback method in the class that give the player number. This is hard coded to 0. I changed it so it reflected either 0 or 1 depending on the _owner's name.
I should get a chance to play with this stuff again in a day or two. I'll post my findings...
It's cherry season here in Australia and I've been busy preserving cherries in my spare time. I love a boozy, thick cherry sauce...
#4
On the face of it your suggestions seems like a reasonable one. Things start getting in the way when you delve into the processing of movement events.
The ProcessTick(Move move, float elapsed) callback is setup from the _InitComponent(TorqueObject owner) method:
The owner is also stored (for later use) in the _InitComponent(TorqueObject owner) method:
when the ProcessTick() callback gets executed the physics get manipulated to make the sprite move:
The problem is that _owner is referenced to make the sprite move. Naturally only one sprite will ever move since there is only one _owner object stored in MovementComponent.
What are the possible solutions to this:
Option 1.
Store another _owner in MovementComponent. I created a _owner0 and _owner1 for this. In _InitComponent(TorqueObject owner) I used the name of the sprite to differentiate:
I am assuming _InitComponent() will get called (by the underlying framework) twice: once for each sprite. I am also assuming that I don't need to change the way the ProcessTick() callback is setup:
Now the problem falls to the ProcessTick(Move move, float elapsed) method. How can it know what _owner to use when manipulating the physics: _owner0 or _owner1? There doesn't seem to be any way to distinguish between the two.
This way of doing 2 players seems like a dead end...
Option 2.
Instead of have 2 _owner's stored in MovementComponent, subclass MovementComponent to create 2 child classes: one for each _owner.
I haven't tested this yet because I'm still trying to work out how to get my subclassed components into TGBX so I can assign the sprites to the subclass instead of the MovementComponent.
Does anyone else think this might work?
Thanks,
Ty
01/20/2007 (2:22 pm)
Thanks Thomas.On the face of it your suggestions seems like a reasonable one. Things start getting in the way when you delve into the processing of movement events.
The ProcessTick(Move move, float elapsed) callback is setup from the _InitComponent(TorqueObject owner) method:
// tell the process list to notifiy us with ProcessTick and InterpolateTick events ProcessList.Instance.AddTickCallback(owner, this);
The owner is also stored (for later use) in the _InitComponent(TorqueObject owner) method:
// retain a reference to this component's owner object _owner = owner as T2DSceneObject;
when the ProcessTick() callback gets executed the physics get manipulated to make the sprite move:
// set our test object's Velocity based on stick/keyboard input _owner.Physics.VelocityX = move.Sticks[0].X * 20.0f; _owner.Physics.VelocityY = -move.Sticks[0].Y * 100.0f;
The problem is that _owner is referenced to make the sprite move. Naturally only one sprite will ever move since there is only one _owner object stored in MovementComponent.
What are the possible solutions to this:
Option 1.
Store another _owner in MovementComponent. I created a _owner0 and _owner1 for this. In _InitComponent(TorqueObject owner) I used the name of the sprite to differentiate:
// retain a reference to this component's owner object
if (owner.Name.Equals("Bat0"))
{
_owner0 = owner as T2DSceneObject;
_playerNumber = 0;
} else if (owner.Name.Equals("Bat1"))
{
_owner1 = owner as T2DSceneObject;
_playerNumber = 1;
}
_SetupInputMap(owner, _playerNumber, "gamepad" + _playerNumber, "keyboard");I am assuming _InitComponent() will get called (by the underlying framework) twice: once for each sprite. I am also assuming that I don't need to change the way the ProcessTick() callback is setup:
// tell the process list to notifiy us with ProcessTick and InterpolateTick events ProcessList.Instance.AddTickCallback(owner, this);
Now the problem falls to the ProcessTick(Move move, float elapsed) method. How can it know what _owner to use when manipulating the physics: _owner0 or _owner1? There doesn't seem to be any way to distinguish between the two.
This way of doing 2 players seems like a dead end...
Option 2.
Instead of have 2 _owner's stored in MovementComponent, subclass MovementComponent to create 2 child classes: one for each _owner.
I haven't tested this yet because I'm still trying to work out how to get my subclassed components into TGBX so I can assign the sprites to the subclass instead of the MovementComponent.
Does anyone else think this might work?
Thanks,
Ty
#5
If I've done anything silly or if this isn't a good way to get 2 player functionality from the StarterGame example please post a reply.
01/20/2007 (3:11 pm)
Well it looks like Option 2 is the way to go. Here's my code.If I've done anything silly or if this isn't a good way to get 2 player functionality from the StarterGame example please post a reply.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using GarageGames.Torque.Core;
using GarageGames.Torque.T2D;
using GarageGames.Torque.Sim;
using GarageGames.Torque.Platform;
namespace StarterGame
{
public class MovementComponent : TorqueComponent, ITickObject
{
//======================================================
#region Constructors
#endregion
//======================================================
#region Public properties, operators, constants, and enums
public int PlayerNumber
{
get { return _playerNumber; }
set { _playerNumber = value; }
}
#endregion
//======================================================
#region Public Methods
public void InterpolateTick(float k)
{
}
public void ProcessTick(Move move, float elapsed)
{
if (move != null)
{
// set our test object's Velocity based on stick/keyboard input
_owner.Physics.VelocityX = move.Sticks[0].X * 100.0f;
_owner.Physics.VelocityY = -move.Sticks[0].Y * 100.0f;
}
}
#endregion
//======================================================
#region Private, protected, internal methods
protected override bool _InitComponent(TorqueObject owner)
{
if (!base._InitComponent(owner) || !(owner is T2DSceneObject))
return false;
return true;
}
#endregion
//======================================================
#region Private, protected, internal fields
protected T2DSceneObject _owner;
protected int _playerNumber = 0;
#endregion
}
public class Player1MoveComponent : MovementComponent
{
protected override bool _InitComponent(TorqueObject owner)
{
if (!base._InitComponent(owner) || !(owner is T2DSceneObject))
return false;
// retain a reference to this component's owner object
_owner = owner as T2DSceneObject;
_playerNumber = 0;
_SetupInputMap(owner, _playerNumber, "gamepad" + _playerNumber, "keyboard");
// tell the process list to notifiy us with ProcessTick and InterpolateTick events
ProcessList.Instance.AddTickCallback(owner, this);
return true;
}
private void _SetupInputMap(TorqueObject player, int playerIndex, String gamePad, String keyboard)
{
// Set player as the controllable object
PlayerManager.Instance.GetPlayer(playerIndex).ControlObject = player;
// Get input map for this player and configure it
InputMap inputMap = PlayerManager.Instance.GetPlayer(playerIndex).InputMap;
int gamepadId = InputManager.Instance.FindDevice(gamePad);
if (gamepadId >= 0)
{
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.LeftThumbX, MoveMapTypes.StickAnalogHorizontal, 0);
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.LeftThumbY, MoveMapTypes.StickAnalogVertical, 0);
}
// keyboard controls
int keyboardId = InputManager.Instance.FindDevice(keyboard);
if (keyboardId >= 0)
{
// Arrow keys
inputMap.BindMove(keyboardId, (int)Keys.Right, MoveMapTypes.StickDigitalRight, 0);
inputMap.BindMove(keyboardId, (int)Keys.Left, MoveMapTypes.StickDigitalLeft, 0);
inputMap.BindMove(keyboardId, (int)Keys.Up, MoveMapTypes.StickDigitalUp, 0);
inputMap.BindMove(keyboardId, (int)Keys.Down, MoveMapTypes.StickDigitalDown, 0);
}
}
}
public class Player2MoveComponent : MovementComponent
{
protected override bool _InitComponent(TorqueObject owner)
{
if (!base._InitComponent(owner) || !(owner is T2DSceneObject))
return false;
// retain a reference to this component's owner object
_owner = owner as T2DSceneObject;
_playerNumber = 1;
_SetupInputMap(owner, _playerNumber, "gamepad" + _playerNumber, "keyboard");
// tell the process list to notifiy us with ProcessTick and InterpolateTick events
ProcessList.Instance.AddTickCallback(owner, this);
return true;
}
private void _SetupInputMap(TorqueObject player, int playerIndex, String gamePad, String keyboard)
{
// Set player as the controllable object
PlayerManager.Instance.GetPlayer(playerIndex).ControlObject = player;
// Get input map for this player and configure it
InputMap inputMap = PlayerManager.Instance.GetPlayer(playerIndex).InputMap;
int gamepadId = InputManager.Instance.FindDevice(gamePad);
if (gamepadId >= 0)
{
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.LeftThumbX, MoveMapTypes.StickAnalogHorizontal, 0);
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.LeftThumbY, MoveMapTypes.StickAnalogVertical, 0);
}
// keyboard controls
int keyboardId = InputManager.Instance.FindDevice(keyboard);
if (keyboardId >= 0)
{
// WASD
inputMap.BindMove(keyboardId, (int)Keys.D, MoveMapTypes.StickDigitalRight, 0);
inputMap.BindMove(keyboardId, (int)Keys.A, MoveMapTypes.StickDigitalLeft, 0);
inputMap.BindMove(keyboardId, (int)Keys.W, MoveMapTypes.StickDigitalUp, 0);
inputMap.BindMove(keyboardId, (int)Keys.S, MoveMapTypes.StickDigitalDown, 0);
}
}
}
}
#6
01/20/2007 (3:24 pm)
I want to write this up in a tutorial. Is a blog (here in the GG site) the best place to do it?
#7
Please just try it, it works:
-Make the changes I suggested to MoveComponent.
-In the editor create 2 objects.
-Put a move component on each object.
-In the editor, set the player field on the move component to 0 on one and 1 on the other.
-Run the level.
If I were uncertain of the outcome or hadn't tested it myself, I would say so. This works and does exactly what you were asking for in a way that the component was originally intended to be used. I fear that a tutorial explaining your methods might do more harm than good, because it makes some innacurate implications as to how components function in general.
01/20/2007 (7:17 pm)
You're not getting it. There's one move component on *each* object, thus the _owner of each move component is the scene object that that move component is on. The _owner field is not static, there are just as many _owner fields as there are move components. Please just try it, it works:
-Make the changes I suggested to MoveComponent.
-In the editor create 2 objects.
-Put a move component on each object.
-In the editor, set the player field on the move component to 0 on one and 1 on the other.
-Run the level.
If I were uncertain of the outcome or hadn't tested it myself, I would say so. This works and does exactly what you were asking for in a way that the component was originally intended to be used. I fear that a tutorial explaining your methods might do more harm than good, because it makes some innacurate implications as to how components function in general.
#8
I'll let you know how it goes.
01/20/2007 (9:44 pm)
Ok, Thomas; I did try it your way but it didn't work. I'll give it another try since you are the expert. This time I'll start from scratch and only make the adjustments you suggested to the code.I'll let you know how it goes.
#9
Now it works. I found the problem. TGBX has a bug: it doesn't set the PlayerNumber in the MovementComponent reliably. If you do this, exactly, you should see the bug:
1. Create a new project based on StarterGame
2. Make the edits you suggested to MovementComponent.cs
3. Open TGBX and load StarterGame.txproj
4. Delete the GG logo in the scene view
5. Drag the GG logo onto the left side of the scene view
6. Drag the GG logo onto the right side of the scene view
7. Select the edit button
8. Select the GG logo on the right
9. Add the MovementComponent to it
10. Select the GG logo on the left
11. Add the MovementComponent to it
12. Change the PlayerNumber to 1
13. Save the level (using the save button in the toolbar)
14. Press the play button in VC# Express
Only 1 of the GG logo sprites will move. If you have a look in the levelData.txscene you will see that both sprites have the same PlayerNumber = 0.
Thanks for persevering with me. I doubt I would have noticed that this was a bug otherwise.
Should I post this in the Open Beta Feedback forum as well?
01/20/2007 (10:13 pm)
Hi Thomas,Now it works. I found the problem. TGBX has a bug: it doesn't set the PlayerNumber in the MovementComponent reliably. If you do this, exactly, you should see the bug:
1. Create a new project based on StarterGame
2. Make the edits you suggested to MovementComponent.cs
3. Open TGBX and load StarterGame.txproj
4. Delete the GG logo in the scene view
5. Drag the GG logo onto the left side of the scene view
6. Drag the GG logo onto the right side of the scene view
7. Select the edit button
8. Select the GG logo on the right
9. Add the MovementComponent to it
10. Select the GG logo on the left
11. Add the MovementComponent to it
12. Change the PlayerNumber to 1
13. Save the level (using the save button in the toolbar)
14. Press the play button in VC# Express
Only 1 of the GG logo sprites will move. If you have a look in the levelData.txscene you will see that both sprites have the same PlayerNumber = 0.
Thanks for persevering with me. I doubt I would have noticed that this was a bug otherwise.
Should I post this in the Open Beta Feedback forum as well?
#10
01/20/2007 (11:55 pm)
I just tried this and TGBX saved out both player fields properly. Is there anything else going on that you can think of that might be causing this?
#11
That's strange. I tried it a couple of times to make sure I typed in the instructions correctly and it failed to save properly every time.
The only programs running (other than the usual virus checker, firewall etc) are VC# Express, TGBX and Firefox).
If I get a chance I'll try it on a clean computer. Mine has a lot of development software on it... and VC# Express crashes every time I close it.
It could be that VC#E just isn't noticing a change in the levelData.txscene file; and when I open the file it gives me a cached version instead of reading a fresh copy from the disk. Seems unlikely though.
I've taken to pressing the save button on the toolbar and then using the menu item afterward. It seems to save properly every time when I do that.
01/21/2007 (12:36 am)
Hi,That's strange. I tried it a couple of times to make sure I typed in the instructions correctly and it failed to save properly every time.
The only programs running (other than the usual virus checker, firewall etc) are VC# Express, TGBX and Firefox).
If I get a chance I'll try it on a clean computer. Mine has a lot of development software on it... and VC# Express crashes every time I close it.
It could be that VC#E just isn't noticing a change in the levelData.txscene file; and when I open the file it gives me a cached version instead of reading a fresh copy from the disk. Seems unlikely though.
I've taken to pressing the save button on the toolbar and then using the menu item afterward. It seems to save properly every time when I do that.
#12
01/21/2007 (10:38 am)
Try clicking on the viewable area with your secelctor tool, after you change the player number... or performing some other action, to that will finalize the data you have entered into the application. then save, that should update the data in c#
#13
01/21/2007 (11:17 am)
Yes, that's a good point. If you enter a value into a textbox in TGBX but do not click something else or whatever, it doesn't always 'take' the value as changed.
#14
01/21/2007 (3:03 pm)
Thanks for the tip. You know that might actually be the problem. I'll give it another try when I get a chance and post my results.
#15
01/21/2007 (5:28 pm)
This is a problem that goes way back in Torque. The level editors back to 1.3 had this problem and maybe even further. I'm pretty sure GG has been working on solving it and thought they had mostly taken care of it. At any rate, long time users of Torque technology are pretty much just in the habit of hitting 'enter' any time they type anything into a text box. Just keep repeating "It's a feature!"
#16
When that's at the top of the list of Torque problems I think everyone will be pretty happy about it. It definitely does make the user experience feel a little clunky. Hopefully soon, guys. Hopefully soon.
Oh wait.. I mean, "It's a feature!" ;)
01/22/2007 (8:42 am)
Doh! We've been talking about all the little GUI things that drive us crazy for a while, but we haven't gotten around to fixing them because every time it comes up someone inevitably says "Well why don't we rewrite the whole GUI system? It has to be done eventually!" and then it gets penciled in for "the future".When that's at the top of the list of Torque problems I think everyone will be pretty happy about it. It definitely does make the user experience feel a little clunky. Hopefully soon, guys. Hopefully soon.
Oh wait.. I mean, "It's a feature!" ;)
#17
01/22/2007 (11:54 am)
Well, finish TXE before worrying about little things that annoy us in TGBX ;)
#18
01/22/2007 (3:56 pm)
Different teams, but thanks for the motivation. :)
#19
It'll be good practice for me and may just help in getting into the Intern program that GG runs...
01/22/2007 (7:42 pm)
If the source code for TGBX is made available in the Pro Indie license then I'm happy to look at this little *feature*.It'll be good practice for me and may just help in getting into the Intern program that GG runs...
#20
01/22/2007 (10:51 pm)
Ty, TGBX is built on TGB. So if you have TGB Pro then you can already have a look at all the nuts and bolts that create this feature. Just in case you wanted to get a head start ;)
Torque 3D Owner Jonathon Stevens