Showing posts with label how to. Show all posts
Showing posts with label how to. Show all posts

Tuesday, July 17, 2018

Simple Arcade Game 4: User Input and Spatial Controls

Welcome back to our simple arcade game tutorial where we look into what it takes to make a very simple, flock herding game using jMonkeyEngine. Today we are going to start adding logic to our game. We are going to start with a simple way to steer the white cube we made last time by reading user input, and applying the movements to our game objects via Controls.

First, let's look at our GameState class. Our GameState extends AbstractAppState and overrides the Initialize method. We also have a custom method to spawn our player object. We are going to add a few more lines in our Initialize method to handle user input.

First let's grab the SimpleApplications input manager. The input manager needs to have Mappings added which we will later listen for, so let's define some static mapping names so they are easy to find later. We then take our InputManager and add our mappings to specific key bindings. For keyboard keys we use a KeyTrigger and grab the keycode from the KeyInput class.

public class GameState extends AbstractAppState{
    //We make the input mappings public and static so they are easy to find later
    public static final String PLAYER_FORWARD = "Forward_Move";
    public static final String PLAYER_BACKWARD = "Backward_Move";
    public static final String PLAYER_LEFT = "Left_Move";
    public static final String PLAYER_RIGHT = "Right_Move";
    private Spatial player;
    private Node scene;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        SimpleApplication application = (SimpleApplication)app;
        AssetManager am = application.getAssetManager();
        Node rootNode = application.getRootNode();
        scene = new Node("Game_Scene");
        rootNode.attachChild(scene);
        
        player = createPlayer(am);
        scene.attachChild(player);
        
        //configure user input
        InputManager input = application.getInputManager();
        input.addMapping(PLAYER_FORWARD, new KeyTrigger(KeyInput.KEY_UP));
        input.addMapping(PLAYER_BACKWARD, new KeyTrigger(KeyInput.KEY_DOWN));
        input.addMapping(PLAYER_LEFT, new KeyTrigger(KeyInput.KEY_LEFT));
        input.addMapping(PLAYER_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT));
    }
    
    ...
}

If you run the game now and start pressing the cursor buttons... nothing happens! We still need to register a listener for these mappings. Let's make GameState implement ActionListener so anything we want to do with player input can be done very easily.

The ActionListener interface has a single method we need to override, the onAction(String name, boolean isPressed, float tpf) method. When we register a listener to the key input, the onAction method gets called from ALL keybindings we register. Therefor, we need to sort through all possible inputs with if or switch statements. I prefer if statements because it is easier to assign blocking vs non blocking keybindings.

public class GameState extends AbstractAppState implements ActionListener{
    ...

    @Override
    public void initialize(AppStateManager stateManager, Application app) {

        ...
        
        //disable the default flycam
        application.getStateManager().getState(FlyCamAppState.class).setEnabled(false);

        //configure user input
        InputManager input = application.getInputManager();
        input.addMapping(PLAYER_FORWARD, new KeyTrigger(KeyInput.KEY_UP));
        input.addMapping(PLAYER_BACKWARD, new KeyTrigger(KeyInput.KEY_DOWN));
        input.addMapping(PLAYER_LEFT, new KeyTrigger(KeyInput.KEY_LEFT));
        input.addMapping(PLAYER_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT));
        //here we register GameState as a listener with the input manager and assign
        //it all the mappings we want to listen to
        input.addListener(this, PLAYER_FORWARD, PLAYER_BACKWARD, PLAYER_LEFT, PLAYER_RIGHT);
    }
    
    ...

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        System.out.println(name+" was pressed");
        //We WANT the forward button press to block a backward button press, so we use if/else
        if(name.equals(PLAYER_FORWARD)){
            
        } else if(name.equals(PLAYER_BACKWARD)){
            
        }
        //we DON'T want the forward or backward buttons to block left and right buttons
        if(name.equals(PLAYER_LEFT)){
            
        } else if(name.equals(PLAYER_RIGHT)){
            
        }
    }
}

Now if you press play you can use the arrow keys to print the name of the binding you are currently pressing to the console. You may also notice the camera moving when you use the arrow keys, this is because SimpleApplication creates a FlyCam by default. It's easy enough to turn off by adding another line to our Initialize Method.

We have our user input registered with our input manager, now how do we get this to control our cube? The simple answer, Controls. Let's make a new package com.mrugames.unit and create a new class, Mob.java. Mobs in game design lingo are simply mobile entities, or anything that moves. That is exactly what our mob control is going to do, move whatever it is attached to at a specific speed every frame. Make our Mob class extend AbstractControl and override the 2 abstract methods controlUpdate(float tpf) and controlRender(RenderManager rm, ViewPort vp). We won't be using control render in our mob class, but it is a good place to put render specific code.

For our Mob class we want to store the maximum speed the mob can move as well as the current input direction for the mob. We will assume the input direction is a number between 0 and 1. Every frame that the input direction is greater than 0, we move the mob by it's maxSpeed*inputDirection*tpf (time per frame).

The Control class has a convenient "spatial" field which you can use to access the Spatial the control is attached to. Spatials also have a convenient "move" method which takes a Vector3f argument.

public class Mob extends AbstractControl{
    public float maxSpeed = 1f;
    public Vector2f dir = new Vector2f(0,0);

    @Override
    protected void controlUpdate(float tpf) {
        //every frame that dir is greater than 0, move in the direction of dir by
        //our max speed
        if(dir.lengthSquared() > 0){
            //spatial is a protected field inherited from AbstractControl
            spatial.move(dir.x*maxSpeed*tpf, dir.y*maxSpeed*tpf, 0);
        }
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        
    }
}

Now that we have our Mob control, we need to attach it to our player Spatial. We can then access our Mob control in our ActionListener to set the Mob.dir to match the players input.

public class GameState extends AbstractAppState implements ActionListener{    

    ...

    private Spatial createPlayer(AssetManager am){
        Mesh mesh = new Box(0.5f,0.5f,1f);
        Geometry geo = new Geometry("Player", mesh);
        Material mat = new Material(am, "Common/MatDefs/Misc/Unshaded.j3md");
        geo.setMaterial(mat);
        //Add a new Mob control to the player
        geo.addControl(new Mob());
        return geo;
    }    

    ...

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        Mob playerMob = player.getControl(Mob.class);
        //We WANT the forward button press to block a backward button press, so we use if/else
        if(name.equals(PLAYER_FORWARD)){
            if(isPressed){
                //Called the frame the button was pressed down
                playerMob.dir.y =1 ;
            } else{
                //Called the frame the button was released
                playerMob.dir.y = 0;
            }
        } else if(name.equals(PLAYER_BACKWARD)){
            if(isPressed){
                playerMob.dir.y = -1;
            } else{
                playerMob.dir.y = 0;
            }
        }
        //we DON'T want the forward or backward buttons to block left and right buttons
        if(name.equals(PLAYER_LEFT)){
            if(isPressed){
                playerMob.dir.x = -1;
            } else{
                playerMob.dir.x = 0;
            }
        } else if(name.equals(PLAYER_RIGHT)){
            if(isPressed){
                playerMob.dir.x = 1;
            } else{
                playerMob.dir.x = 0;
            }
        }
    }

As you see there are some simple tricks we use to process the player input. The ifPressed variable will be true the frame that the player activates the mapping, and false on the frame that the mapping is deactivated. This makes on/off functionality easy. If you need to be notified every frame that a mapping is being activated, take a look at the AnalogListener class. We will cover the AnalogListener in more detail later.

Go ahead and press play! You'll see our little cube drift around the screen corresponding to your button presses.

Monday, July 2, 2018

Simple Arcade Game 3: Scenegraph

Hello again and welcome to another jMonkey Engine tutorial! Today we are going to take a detailed look at probably the most important aspect of jMonkey Engine, it's Scenegraph.

A great overview of how jme uses it's scenegraph is available here, but I'll run a brief overview for those who want to dive right in.

The Scene in jme is made up of Spatials, mostly Nodes and Geometries. Nodes do not have any visual elements, but can have children elements. Geometries have meshes and materials and will be rendered, these are the elements you actually see! All spatials must be connected to the RootNode, which is the top most node in the scene. You can easily make complex or large scale models with many moving parts by mixing Nodes and Geometries.

The most important thing to remember: DO NOT EXTEND NODE, GEOMETRY OR SPATIAL. Instead you should store your game data seperately, in the Spatial's UserData container, or as a Control which is attached to a spatial.

UserData can be any Object and is easily written by using Spatial.setUserData(String key, Object data), and later retrieved using Spatial.getUserData(String key).

Controls are a lot like AppStates, except they only work on the spatial they are attached to. We will go over Controls in a later tutorial, but if you are interested in making your own controls now they are easily attached to a spatial using Spatial.addControl(Control control), and retrieved by using Spatial.getControl(Class<T> controlType) where T extends Control.

We are going to make a new appstate for our game, let's call it "GameState". We need to override the "initialize" method as this is where we will be modifying our scene.


public class GameState extends AbstractAppState{
    private Spatial player;
    private Node scene;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        SimpleApplication application = (SimpleApplication)app;
        AssetManager am = application.getAssetManager();
        Node rootNode = application.getRootNode();
        scene = new Node("Game_Scene");
        rootNode.attachChild(scene);
    }
    
}

Lets go ahead and cast the Application app to a SimpleApplication so we can access our RootNode and the AssetManager. We should also define a Spatial object that will reference the Players spatial so we can easily find it later. We also define a Node for the Scene. Create a new Node with a string argument for the node's name and attach it to the root node via the Node.attachChild() method. We will be attaching all the game objects to the game scene instead of the root node to keep them separated and easy to work with!

Now let's define a new method to create our player object. We will eventually be using a Dragon model, but for now let's simple spawn a box to represent our player. First we define a new method, createPlayer(AssetManager am). We are going to pass the assetManager from our Application object to this method to allow the use of pre-defined material definitions which really speed this process.

public class GameState extends AbstractAppState{

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        SimpleApplication application = (SimpleApplication)app;
        AssetManager am = application.getAssetManager();
        Node rootNode = application.getRootNode();
        scene = new Node("Game_Scene");
        rootNode.attachChild(scene);

        player = createPlayer(am);
        scene.attachChild(player);
    }

    private Spatial createPlayer(AssetManager am){
        Mesh mesh = new Box(0.5f,0.5f,1f);
        Geometry geo = new Geometry("Player", mesh);
        Material mat = new Material(am, "Common/MatDefs/Misc/Unshaded.j3md");
        geo.setMaterial(mat);
        return geo;
    }
}

Spawning a model is as simple as creating a geometry, giving it a mesh and a material. Here we use the Box shape to create our mesh. With 3 floating point arguments we construct a box that extends into the x, y and z axis by the given amount. This will give us a box that is actually 1x1x2. Simply pass the mesh to the Geometry as an argument along with a String name to attach the mesh to the geometry.

Next we create the material. Here we are using a Material Definition which is how jmonkey defines what pixel and vertex shaders control the material. We are using the built-in material definition for Unshaded objects by simply defining the path to the j3md file. You can view other material definitions included in the jme3 library in the jme3-core.jar/Common/MatDefs. Call Geometry.setMaterial() and we are finished.

In the initialize method add the 2 lines player = createPlayer(am) and scene.attachChild(player).

This is enough to have our GameState spawn a player object and attach it to the GameScene node, which in turn is attached to the RootNode and will be rendered. Now we need to attach the GameState to the AppStateManager, let's do this in our MainMenu state so in the future we can have a button start the game.

Open our MainMenu appstate and add the following lines.

public class MainMenu extends AbstractAppState {
    private AppStateManager stateManager;
    private float countDown = 3f;
    private BitmapText text;

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.stateManager = stateManager;
        
        ...
    }
    
    @Override
    public void update(float tpf) {
        if((countDown -= tpf) <= 0){
            stateManager.detach(this);
            stateManager.attach(new GameState());
        }
    }
    
    ...
}

First we need to define 2 new fields, stateManager and countDown. The state manager is passed via the initialize() method when the AppState is attached to the AppStateManager. Go ahead and assign this via "this.stateManager = stateManager" so we can access it later.

Then in our update loop we are going to count down from 3. Once our countdown is equal to or less than 0, we will remove MainMenu from our AppStateManager and attach a new GameState. Do this by simple subtracting tpf from countDown once a frame and checking with a basic if statement. Remember, in a video game you NEVER want to use wait() as this will completely lock your thread from doing other things. Making simple timers like the one above is easy and guarantees that your other appstates have a chance to  run their updates while we wait.

Now if you press Play you will see our lovely Main Menu for approximately 3 seconds before being taken to our game state, and spawning in a simple, white cube!

Friday, April 13, 2018

Simple Arcade Game 2: Intro to Appstates

Welcome back to another lesson in making a simple arcade game in jMonkey Engine. last time we got the jme sdk installed and created our project. Today we will look at jMonkeys powerful appstates, input handling and a first glance at the scenegraph.

Let's open up our project and take a look at the example class that was provided.

public class Main extends SimpleApplication {

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
    }

    @Override
    public void simpleUpdate(float tpf) {
        //TODO: add update code
    }

    @Override
    public void simpleRender(RenderManager rm) {
        //TODO: add render code
    }
}

The first part is our java main method. This is called by the jvm, and here we create our jmonkey application.

Next is the SimpleApplication method, simpleInitApp(). This is called after the application is created and has finished starting up all of jmonkey's dependencies, but before simpleUpdate and simpleRender. Here you can initialize the game state, create game objects and modify the scene graph. By default it creates a box mesh, wraps it in a geometry, assigns a blue material to the geometry and attaches the geometry to the scene graph. We'll discuss geometries and the scene graph in more detail later.

Next up is the simpleUpdate method from SimpleApplication. SimpleUpdate is called once every frame and is where you should store game logic that happens over time. Frames are called as fast as possible, and while your game is small it's very possible to see 4000+ frames called every second. The argument "tpf" (Time Per Frame) is used to provide you with the amount of time, in seconds, the previous frame took to execute. This value is what you will use to move objects smoothly and consistently.

The last method created by default is the simpleRender method. We will not be using the simpleRender method for our simple arcade game and it is safe to simply remove it for now. SimpleRender is called after the simpleUpdate method and can be used to modify how the scene is rendered through the RenderManager. It is not safe to modify the scenegraph from this method.

You may be tempted to start writing your game logic and putting it all in the nice simpleUpdate method, and you wouldn't be wrong to do so. Super small games can easily exist entirely within one class and you could get away with using your SimpleApplication for most of your game logic, but did you know there's a better way?

JMonkey provides us with a concept called "App States". An appstate is a confined version of simpleInit and simpleUpdate which can be attached to a simple application and every frame all of your appstates will be executed before calling simpleUpdate.

Why would you want to use an appstate? The answer is simple, you can seperate un related code, easily pause/unpause or remove game logic simply by attaching or removing your appstates. You can have a main menu appstate, which you disable when you start your game appstate. Your game appstate could create several additional app states to process the game state. When you press the pause button you can easily pause only the game appstate while activating a new pause menu appstate and so on. Appstates make game design easy and robust and are one of jmonkey's most powerful features. Let's go ahead and create a main menu state right now!


In the sdk right click the package that contains your Main class and select "New/Other." From there select JME3 Classes category and under File Type choose "New AppState."


Let's name our new appstate "MainMenu" and click finish. You should have a class named "MainMenu" with the following code:

public class MainMenu extends AbstractAppState {
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        //TODO: initialize your AppState, e.g. attach spatials to rootNode
        //this is called on the OpenGL thread after the AppState has been attached
    }
    
    @Override
    public void update(float tpf) {
        //TODO: implement behavior during runtime
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        //TODO: clean up what you initialized in the initialize method,
        //e.g. remove all spatials from rootNode
        //this is called on the OpenGL thread after the AppState has been detached
    }
}

Looks familiar doesn't it? You have an initialize method which is called before any update method the frame the appstate gets added to your application. Next is the update method, called once a frame before the SimpleApplication's update.

The last method is a new one, cleanup(). Cleanup is called before any update on the frame after an appstate has been removed from the application. This is how you can remove any objects associated with your appstate from the scenegraph.

Now lets have this appstate display something when we activate it!

Out of the box JME doesn't have the most robust built in gui library, but it does have plenty to get us started. We'll make use of the BitmapFont and BitmapText classes to display a simple label on the screen.

First let's add a private field to MainMenu to hold a reference to our BitmapText object. Then we need to load the default font file provided with jme. To load assets we'll use the Applications asset manager. BitmapFont has a method, "createLabel" that will create a jme spatial we can attach to the scene graph. To display the text we need to attach it to SimpleApplications default gui node. We'll need to cast app to accomplish this.

public class MainMenu extends AbstractAppState {
    private BitmapText text;

    @Override
    public void initialize(AppStateManager stateManager, Application app){
        //jME has a built in font which can be loaded from "Interface/Fonts/Default.fnt"
        BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
        text = font.createLabel("Hello jMonkey Engine!");
        ((SimpleApplication)app).getGuiNode().attachChild(text);
    }

This will attach our text to the origin point of the Gui Node. We'll go more in depth with the scene graph next time, for now you only need to know there are 2 scenes included with SimpleApplication, the Gui scene and the Root scene.

Let's hop back to our Main class and create an instance of our appstate. Go ahead and delete everything in simpleInitApp(), we don't want the blue cube anymore.


public class Main extends SimpleApplication {

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        MainMenu menu = new MainMenu();
        stateManager.attach(menu);
        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
    }

    @Override
    public void simpleUpdate(float tpf) {
        //TODO: add update code
    }
}

Now if you hit play, you don't see anything. This is because of how jmonkey handles the gui scene. Our text is spawning at 0,0 and going DOWN, which pushes it off of our screen! not good, let's fix it! Go back to the MainMenu appstate and let's move the text.

public class MainMenu extends AbstractAppState {
    private BitmapText text;
    
    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        
        BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
        text = font.createLabel("Hello jMonkey Engine!");
        
        //set the text to the center of the screen
        Camera cam = app.getCamera();
        int width = cam.getWidth();
        int height = cam.getHeight();
        float x = (width/2)-(text.getLineWidth()/2);
        float y = (height/2)+(text.getLineHeight()/2);
        text.setLocalTranslation(x, y, 0);

        ((SimpleApplication)app).getGuiNode().attachChild(text);
    }
    
    @Override
    public void update(float tpf) {
        //TODO: implement behavior during runtime
    }
    
    @Override
    public void cleanup() {
        super.cleanup();
        text.removeFromParent();
    }
}

Wait, that's a lot of new code! It's ok, first we need our applications resolution, the easiest way to get this is from our camera's width and height. Next we find the center. The gui origin is the bottom left, and text grows from the top down. Lastly we need to set the translation with setLocalTranslation.

Now is also a good time to remove the text from the scenegraph in our cleanup method, this way when the appstate is closed the text will go with it!

Now try pressing play. Your message should be nicely visible in the center of the screen!


That's the basics to Appstates, you can create them, attach and detach them to add and remove game behaviors. There is also a convenient setEnabled() method you can use to pause an appstate you don't want to remove completely. We'll be making extensive use of appstates to get our simple arcade game running well!

Saturday, March 31, 2018

Simple Arcade Game 1: Installing JME

Welcome to part 1 of making a simple arcade game in jMonkey Engine! In this series we are going to look at making a simple top down game about herding sheep. The focus of these tutorials will be primarily on learning how to use JME to accomplish simple game mechanics and get you started on a project of your own. We'll cover topics such as app states, controls, the scene graph, transforms in 3d space, the asset pipeline and any other topics that crop up while we are finishing this simple game. Before we can dive into writing our first game, we need to get jMonkey Engine to run!

JME is a java library, and of course you will need a version of jdk found here. I'll assume you have at least jdk8 installed for these tutorials.

The simplest way to work with jMonkey Engine is to install the latest community SDK. The JME SDK is a customized version of the Netbeans IDE with some very helpful features that will make working with JME much simpler. It is possible to use your own IDE, or even a text editor and compile on your own, but for this series I will assume you are using the provided SDK and it's helpful JME integrations.

Go ahead and download the jMonkey SDK here.

If you are wondering why my screenshots look different, it's because I use the awesome "DarkMonkey" plugin available under "Tools/Plugins" You may want to look through what plugins are available and see if there are any that interest you or might make development easier.

Once you have the SDK installed you will need to create a new project. In the top left go to "File/New Project..." under Categories choose "JME3" and under Projects choose "Basic Game."



Click Next and choose a name for the project and a folder to store all the project files in. I am naming this project "SheepTut" and storing it in a folder on my secondary drive. Be sure you have access to the folder you choose as you will be adding and removing files often.

Now you have your project created, it should be visible on the left under the "Projects" tab. If for some reason you don't see the projects tab, go to "Windows/Projects" or press "CTRL-1" to open a new projects tab. I keep this docked on the left side for convenience. If you open up all of the project folders you should see something like this:

If your source packages and libraries look different, right click in a blank area in the Projects tab and select "View Java Packages As..." and select "Reduced Tree."

So what are we looking at here? Well first is the Project Assets folder. This folder will be packaged inside the .jar file when you build the project and is where you will be storing all of your game assets, 3d models, textures, sound files, etc. It comes pre-filled with some sub-folders which are all empty by default. This is fine, we'll be adding our own files soon enough.

Next is the Source Packages. These are all of your Java class files and where you will actually be building your game. Right now it has 1 package called "mygame" with 1 Main class. Feel free to create a new package if you would prefer something more descriptive. Java convention states packages should be named after their "reverse internet domain name" so I'll go ahead and create a new package called "com.mrugames." and click and drag the Main class into my new package.


When clicking and dragging a java class it will ask if you wish to refactor, click yes to let the SDK handle formatting the class to it's new package.

The next thing listed in your project tab is the Libraries. This contains all the jMonkey Engine libraries you will need to run your game, as well as your JDK libraries for the version of Java that is installed. If you wish to change the JDK simply right click on your project, go to properties and under Source/Binary file select the JDK you wish to use. If you do not see the version of JDK you were hoping to use and it is installed on your system, then go to Tools/Java Platforms, Add Platform and follow the wizard to locate your JDK.

The last folder listed is called "Important Files." This contains your build file which you will probably not need to edit. The SDK should be able to modify this file for you as needed through project properties. If you do find yourself needing to modify the build script, this is the quickest way to find it.

Now that you are familiar with the jMonkey project, let's open the Main class that was generated an run it! Simply double click Main.java and you will be greeted with a class that extends Simple Application and has 4 methods, a main(), a simpleInitApp(), a simpleUpdate() and a simpleRender(). You should be able to press Run Project(F6) and be greeted by the jMonkeyEngine splash screen!



Press continue to finally run your first jMonkey Engine game:


Congrats! You managed to get jMonkey Engine SDK to run and created your first project! If you had any difficulties getting this far feel free to leave a comment either here, on this blog or at the jMonkey Engine forum where either myself or someone from the super helpful community can point you in the right direction.

Next time we will take a closer look at the Simple Application class, the BaseAppState class and how jMonkey handles frame updates!